# Deployment Guide > **From source to production** — how every Arcline service is deployed, > managed, and maintained. --- ## Table of Contents - [Overview](#overview) - [Production Environment](#production-environment) - [Deployment Methods](#deployment-methods) - [Service Directory Layout](#service-directory-layout) - [User & Permissions Model](#user--permissions-model) - [Systemd Service Units](#systemd-service-units) - [Nginx Reverse Proxy](#nginx-reverse-proxy) - [Deploy by Service](#deploy-by-service) - [Database Management](#database-management) - [Backup & Restore](#backup--restore) - [Rollback Procedures](#rollback-procedures) - [Health Checks](#health-checks) - [Troubleshooting](#troubleshooting) --- ## Overview Every Arcline service deploys as a single static Go binary. The deployment strategy depends on the service type: | Type | Method | Examples | |------|--------|---------| | **Web service** | Docker container | portal, billing, docs | | **Native binary** | Direct install on host | website (OpenBSD), uptime | | **CLI tool** | Install to `$PATH` | check, audit, dns, migrate, vault | All services share the same core deployment flow: 1. **Build** — Compile static Go binary with `CGO_ENABLED=0` 2. **Package** — Containerize (Docker) or ship raw binary 3. **Deploy** — Transfer to production host and start 4. **Verify** — Health check endpoint or process monitoring --- ## Production Environment ### Host Specifications | Host | Role | OS | Location | |------|------|----|----------| | `srv01` | Primary web server | OpenBSD | Arcline datacenter | | `srv02` | Application server | Linux (Alpine) | Arcline datacenter | | `srv03` | Database & storage | Linux (Alpine) | Arcline datacenter | ### Network - All services listen on **127.0.0.1** (localhost) only - Nginx handles TLS termination and reverse proxy to local services - Public access is through pfSense port forwarding (443 → Nginx) ### Runtime Dependencies - **Docker** (for containerized services): `docker-ce` - **Nginx**: `nginx` (OpenBSD: `nginx` package) - **SQLite**: No runtime dependency (pure Go, file-based) - **ca-certificates**: Required in all containers for TLS --- ## Deployment Methods ### Method 1: Docker Deployment (Web Services) Used for: portal, billing, docs (dynamic), email (future) ```bash # ── On production host ──────────────────────────────────────────────── # 1. Pull the latest image docker login -u -p docker pull /:latest # 2. Stop and remove existing container docker stop 2>/dev/null || true docker rm 2>/dev/null || true # 3. Start new container docker run -d \ --name \ --restart unless-stopped \ -p 127.0.0.1:: \ -v /opt//.env:/app/.env:ro \ -v /opt//data:/app/data \ /:latest # 4. Verify docker ps | grep docker logs --tail 20 ``` ### Method 2: Native Binary (OpenBSD) Used for: website ```bash # ── On build machine ────────────────────────────────────────────────── # Cross-compile GOOS=openbsd GOARCH=amd64 go build -ldflags="-s -w" -o binary-openbsd-amd64 . # Deploy scp binary-openbsd-amd64 srv01:/usr/local/bin/ rsync -av --delete static/ srv01:/var/www//static/ # ── On production host ──────────────────────────────────────────────── # Restart service doas rcctl restart ``` ### Method 3: Static Binary (CLI Tools) Used for: uptime, status, check, audit, dns, migrate, vault ```bash # ── On build machine ────────────────────────────────────────────────── # Cross-compile for the target architecture GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o binary-linux-amd64 . # Deploy scp binary-linux-amd64 srv02:/usr/local/bin/ ssh srv02 "chmod 0755 /usr/local/bin/" # Verify ssh srv02 " version" ``` --- ## Service Directory Layout ### Containerized Services ``` /opt/arcline-/ ├── .env # Environment variables (arcline:arcline, 0640) └── data/ # Persistent data directory └── .db # SQLite database (created at runtime) ``` ### Native Services (OpenBSD) ``` /usr/local/bin/ ├── arcline-web # Go binary └── rc.d/ # rc.d scripts (managed by rcctl) /var/www/arclineit/ ├── static/ # CSS, JS, images ├── templates/ # Go HTML templates └── .env # Environment config ``` ### CLI Tools ``` /usr/local/bin/ ├── arcline-uptime ├── arcline-status ├── arcline-check ├── arcline-audit ├── arcline-dns ├── arcline-migrate ├── arcline-vault └── arcline-email /etc/arcline/ ├── uptime.yaml # Uptime monitor config ├── status.yaml # Status page config ├── status.d/ # Modular status config directory └── email.toml # Email server config /var/lib/arcline/ ├── uptime.db # Uptime monitoring database └── vault/ # Vault secrets storage ``` --- ## User & Permissions Model ### System User ```bash # Create the arcline user (done once per host) addgroup -S arcline adduser -S -G arcline -h /opt arcline ``` ### Directory Permissions ```bash # Application directory chown -R arcline:arcline /opt/arcline- chmod 0750 /opt/arcline- # Environment file (sensitive!) chown arcline:arcline /opt/arcline-/.env chmod 0640 /opt/arcline-/.env # Database file (created by app, but ensure permissions) chown arcline:arcline /opt/arcline-/data/*.db chmod 0640 /opt/arcline-/data/*.db ``` ### Docker Container User All Docker containers run as the `arcline` non-root user: ```dockerfile RUN addgroup -S arcline && adduser -S -G arcline arcline USER arcline ``` --- ## Systemd Service Units ### Template: `arcline-.service` ```ini [Unit] Description=Arcline After=network.target [Service] Type=simple User=arcline Group=arcline WorkingDirectory=/opt/arcline- EnvironmentFile=/opt/arcline-/.env ExecStart=/opt/arcline-/ [flags] Restart=on-failure RestartSec=5s # Hardening NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict ProtectHome=yes ReadWritePaths=/opt/arcline- [Install] WantedBy=multi-user.target ``` ### Service Management ```bash # Install the unit file sudo cp arcline-.service /etc/systemd/system/ sudo systemctl daemon-reload # Enable on boot sudo systemctl enable arcline- # Start / stop / restart / status sudo systemctl start arcline- sudo systemctl stop arcline- sudo systemctl restart arcline- sudo systemctl status arcline- # View logs sudo journalctl -u arcline- -f ``` ### Hardening Options | Option | Purpose | |--------|---------| | `NoNewPrivileges=yes` | Prevent privilege escalation via `suid` binaries | | `PrivateTmp=yes` | Isolated `/tmp` for the service | | `ProtectSystem=strict` | Read-only `/usr` and `/etc` | | `ProtectHome=yes` | No access to `/home`, `/root` | | `ReadWritePaths=...` | Only allow writes to the data directory | **Reference implementation:** `portal/deploy/arcline-portal.service` --- ## Nginx Reverse Proxy ### Template: `nginx-.conf` ```nginx # HTTP → HTTPS redirect server { listen 80; server_name ; return 301 https://$host$request_uri; } # HTTPS server server { listen 443 ssl; server_name ; ssl_certificate /etc/ssl//fullchain.pem; ssl_certificate_key /etc/ssl//privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; # Security headers add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; add_header X-Frame-Options DENY always; add_header X-Content-Type-Options nosniff always; location / { proxy_pass http://127.0.0.1:; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 30s; } } ``` ### SSL Certificate Management Certificates from **Let's Encrypt** (certbot) or **Step CA** (internal). ```bash # External (Let's Encrypt) certbot certonly --webroot -w /var/www/acme -d # Internal (Step CA) step certificate install /etc/step-ca/certs/intermediate_ca.crt ``` **Reference implementation:** `portal/deploy/nginx-portal.conf` --- ## Deploy by Service ### website — `arcline.it` | Detail | Value | |--------|-------| | **Method** | Native binary on OpenBSD | | **Binary** | `/usr/local/bin/arcline-web` | | **Static files** | `/var/www/arclineit/static/` | | **Service manager** | OpenBSD `rcctl` | | **Deploy command** | `make deploy` | ```bash # Quick deploy make cross scp arcline-web-openbsd-amd64 srv01:/usr/local/bin/arcline-web rsync -av --delete static/ srv01:/var/www/arclineit/static/ ssh srv01 "rcctl restart arcline-web" ``` ### billing — `client.arcline.it` | Detail | Value | |--------|-------| | **Method** | Docker container | | **Container name** | `arcline-billing` | | **Internal port** | 8082 | | **Data volume** | `/opt/arcline-billing/.env:/app/.env:ro` | | **Database** | SQLite in container (ephemeral — backup on host) | ```bash docker run -d \ --name arcline-billing \ --restart unless-stopped \ -p 127.0.0.1:8082:8082 \ -v /opt/arcline-billing/.env:/app/.env:ro \ /arcline-billing:latest ``` ### portal — `portal.arclineit.com` | Detail | Value | |--------|-------| | **Method** | Docker container | | **Container name** | `arcline-portal` | | **Internal port** | 8082 | | **Data volume** | `/opt/arcline-portal/.env:/app/.env:ro` | | **Database** | SQLite in container (ephemeral — backup on host) | ```bash docker run -d \ --name arcline-portal \ --restart unless-stopped \ -p 127.0.0.1:8082:8082 \ -v /opt/arcline-portal/.env:/app/.env:ro \ /arcline-portal:latest ``` ### git — `git.arcline.it` | Detail | Value | |--------|-------| | **Method** | Docker container | | **Container name** | `arcline-gitea` | | **Internal ports** | 3000 (web), 22 (SSH) | | **Data volume** | `/opt/arcline-gitea/data:/data` | | **Database** | SQLite (or PostgreSQL for multi-instance) | ```bash # Deploy with docker-compose docker compose -f git/deploy/docker-compose.yml up -d # Or manual docker run -d \ --name arcline-gitea \ --restart unless-stopped \ -p 127.0.0.1:3000:3000 \ -p 22:22 \ -v /opt/arcline-gitea/data:/data \ -e DOMAIN=git.arcline.it \ -e ROOT_URL=https://git.arcline.it \ -e SSH_DOMAIN=git.arcline.it \ gitea/gitea:latest # Nginx config (redirect + proxy) sudo cp git/deploy/nginx-git-redirect.conf /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx # Verify curl -I https://git.arcline.it ssh -T _gitea@git.arcline.it ``` ### uptime — Internal Monitor | Detail | Value | |--------|-------| | **Method** | Static binary or Docker | | **Binary** | `/usr/local/bin/arcline-uptime` | | **Config** | `/etc/arcline/uptime.yaml` | | **Database** | `/var/lib/arcline/uptime.db` | ```bash # As a service arcline-uptime start --config /etc/arcline/uptime.yaml # Or via systemd [Service] ExecStart=/usr/local/bin/arcline-uptime start --config /etc/arcline/uptime.yaml ``` ### status — `status.arclineit.com` | Detail | Value | |--------|-------| | **Method** | Static HTML generation | | **Binary** | `/usr/local/bin/arcline-status` | | **Config** | `/etc/arcline/status.yaml` + `/etc/arcline/status.d/` | | **Output** | `/var/www/status/` (served by Nginx) | ```bash # Generate status page arcline-status build --config /etc/arcline/status.yaml --out /var/www/status/ # Watch mode (regenerate on config change) arcline-status build --config /etc/arcline/status.yaml --out /var/www/status/ --watch ``` ### docs — `docs.arclineit.com` | Detail | Value | |--------|-------| | **Method** | Docker container (dynamic) or Nginx (static) | | **Container name** | `arcline-docs` | | **Internal port** | 8080 | ```bash # Dynamic variant (Go server with Markdown rendering) docker run -d \ --name arcline-docs \ --restart unless-stopped \ -p 127.0.0.1:8080:8080 \ -v /opt/arcline-docs/.env:/app/.env:ro \ /arcline-docs:latest # Static variant (pre-built HTML via Dockerfile.static) docker run -d \ --name arcline-docs \ --restart unless-stopped \ -p 127.0.0.1:80:80 \ /arcline-docs:latest-static ``` --- ## Database Management ### Backup ```bash # SQLite databases — simple file copy cp /opt/arcline-/data/.db /backup/-$(date +%Y%m%d).db # Compress gzip /backup/-*.db ``` ### Restore ```bash # Stop the service first docker stop arcline- # or systemctl stop arcline- # Restore database cp /backup/-.db.gz /opt/arcline-/data/ gunzip /opt/arcline-/data/-.db.gz mv /opt/arcline-/data/-.db /opt/arcline-/data/.db chown arcline:arcline /opt/arcline-/data/.db # Restart docker start arcline- # or systemctl start arcline- ``` ### Maintenance ```bash # Vacuum SQLite database (reclaim space) sqlite3 /opt/arcline-/data/.db "VACUUM;" # Integrity check sqlite3 /opt/arcline-/data/.db "PRAGMA integrity_check;" ``` --- ## Backup & Restore ### What to Back Up | Asset | Location | Frequency | |-------|----------|-----------| | SQLite databases | `/opt/arcline-*/data/*.db` | Daily | | Environment files | `/opt/arcline-*/.env` | On change | | SSL certificates | `/etc/ssl/` | On renewal | | Nginx configs | `/etc/nginx/` | On change | | Systemd units | `/etc/systemd/system/arcline-*.service` | On change | | Status config | `/etc/arcline/` | On change | | Uptime config | `/etc/arcline/uptime.yaml` | On change | ### Backup Script ```bash #!/bin/sh # /usr/local/bin/arcline-backup BACKUP_DIR="/backup/arcline/$(date +%Y-%m-%d)" mkdir -p "$BACKUP_DIR" # Databases for db in /opt/arcline-*/data/*.db; do cp "$db" "$BACKUP_DIR/" done # Environment files (mask secrets) for env in /opt/arcline-*/.env; do cp "$env" "$BACKUP_DIR/$(basename $(dirname $env)).env" done # Configs cp -r /etc/arcline "$BACKUP_DIR/" # Compress tar czf "$BACKUP_DIR.tar.gz" -C "$(dirname $BACKUP_DIR)" "$(basename $BACKUP_DIR)" rm -rf "$BACKUP_DIR" # Sync to backup host rsync -av "$BACKUP_DIR.tar.gz" backup-host:/backups/arcline/ ``` --- ## Rollback Procedures ### Docker Rollback ```bash # 1. Check previous image tags docker images / # 2. Deploy a specific tag instead of latest docker run -d \ --name -rollback \ /: # 3. Verify health curl http://127.0.0.1:/health # 4. Swap if healthy docker stop docker rm docker rename -rollback ``` ### Native Binary Rollback ```bash # 1. Keep previous binary versions cp /usr/local/bin/ /usr/local/bin/.bak # 2. Restore previous version cp /usr/local/bin/.bak /usr/local/bin/ # 3. Restart service systemctl restart # or rcctl restart ``` --- ## Health Checks ### Docker Health Check (in Dockerfile) ```dockerfile HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 ``` ### Manual Health Check ```bash # Web service curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:/health # Process check pgrep -x # Docker check docker ps --filter "name=" --filter "status=running" ``` --- ## Troubleshooting ### Service Won't Start ```bash # Check systemd logs journalctl -u arcline- -f # Check Docker logs docker logs arcline- # Check binary directly /opt/arcline-/ 2>&1 # Common issues: # - Missing .env file → ExecStart fails # - Port conflict → change port in .env # - Permission denied → check chown/chmod ``` ### Database Issues ```bash # Check SQLite integrity sqlite3 /opt/arcline-/data/.db "PRAGMA integrity_check;" # Expected: "ok" # Check disk space df -h /opt/arcline-/data/ # Check file permissions ls -la /opt/arcline-/data/.db # Expected: -rw-r----- arcline arcline ``` ### Connection Refused ```bash # Check if service is listening ss -tlnp | grep # Check Nginx is running systemctl status nginx # Check pfSense NAT rules # Verify port forwarding from WAN:443 → host:443 ``` ### Quick Recovery ```bash # Universal restart sequence docker stop arcline- 2>/dev/null docker rm arcline- 2>/dev/null docker pull /:latest docker run -d --restart unless-stopped \ -p 127.0.0.1:: \ -v /opt/arcline-/.env:/app/.env:ro \ /:latest ```