# Deployment Notes ## Runtime shape Recommended production layout: - run the Go app as a single container; - bind the app container only to `127.0.0.1:8080`; - terminate HTTPS in front of it with Caddy or Nginx; - persist `/data` so both SQLite and artifacts survive restarts; - expose only the reverse proxy to the internet. This repository now includes: - `Dockerfile` - `docker-compose.yml` - `deploy/Caddyfile.example` - `deploy/nginx.update-server.conf.example` - `deploy/update-server.env.example` Important deployment assumptions: - set `APP_BASE_URL` to the public `https://...` URL; - leave `TRUST_PROXY_HEADERS=true` only when the app is actually behind a trusted reverse proxy; - do not mount `/data` into a public web root; - do not serve artifacts directly from Caddy or Nginx. Downloads must continue flowing through `/api/v1/releases/{id}/download`. ## Data layout Persist the whole `/data` mount. Expected contents: - `/data/db.sqlite` - `/data/db.sqlite-wal` - `/data/db.sqlite-shm` - `/data/artifacts/...` The app uses SQLite in WAL mode, so cold file copies must include `db.sqlite-wal` and `db.sqlite-shm` unless you use an online SQLite backup. ## Docker Compose 1. Copy `deploy/update-server.env.example` to a private env file or export the variables in your shell. 2. Set at least: - `APP_BASE_URL` - `ADMIN_EMAIL` - `ADMIN_PASSWORD` 3. Start the app: ```bash docker compose up -d --build ``` Security defaults in the provided compose file: - non-root container user; - read-only root filesystem; - writable `/data` volume only; - writable `tmpfs` at `/tmp` for multipart form handling; - `no-new-privileges`; - all Linux capabilities dropped; - app port bound only to loopback. The current implementation uses database-backed random session tokens, so there is no separate `SESSION_SECRET` environment variable to set. ## Reverse proxy Use one of the example proxy configs and keep the app on loopback. Caddy: - example file: `deploy/Caddyfile.example` - automatic HTTPS is the simplest option for first rollout. Nginx: - example file: `deploy/nginx.update-server.conf.example` - remember to provision certificates separately. Recommended proxy behavior: - pass `Host`, `X-Forwarded-For`, `X-Forwarded-Host`, and `X-Forwarded-Proto`; - keep `client_max_body_size` or equivalent aligned with `MAX_UPLOAD_BYTES`; - optionally IP-allow-list `/admin` if the admin UI is only for operators; - do not add any direct `/artifacts` static mapping. ## Proxmox notes Safe first production rollout on Proxmox: 1. Use a VM or an unprivileged LXC dedicated to this service. 2. Put persistent app data on a host path such as `/srv/update-server/data`. 3. Put backups on a different filesystem or datastore such as `/srv/update-server/backups`. 4. Run the app container on the guest and keep it bound to `127.0.0.1:8080`. 5. Run Caddy or Nginx on the same guest or on a separate reverse-proxy guest. 6. Expose only ports `80` and `443` publicly. 7. Keep `APP_BASE_URL` on the final public HTTPS hostname before testing cookies or HSTS. Operational advice for Proxmox: - snapshots are useful, but they are not a replacement for app-aware backups; - if you use LXC, make sure the mounted `/data` path is writable by the container user; - keep the reverse proxy and app logs outside the repo checkout; - test one admin login, one upload, and one authenticated download after each upgrade. ## Backup Preferred live-backup flow: 1. Use SQLite's online backup command so you get a consistent `db.sqlite` snapshot without stopping the app. 2. Back up `/data/artifacts` separately. Example with Docker: ```bash backup_dir=/srv/update-server/backups/$(date +%Y%m%d-%H%M%S) mkdir -p "${backup_dir}" docker exec update-server sqlite3 /data/db.sqlite ".backup '/tmp/db.sqlite'" docker cp update-server:/tmp/db.sqlite "${backup_dir}/db.sqlite" docker exec update-server rm -f /tmp/db.sqlite tar -C /srv/update-server/data -czf "${backup_dir}/artifacts.tar.gz" artifacts ``` If you take a cold backup instead: 1. stop the container; 2. copy `/data/db.sqlite`, `/data/db.sqlite-wal`, `/data/db.sqlite-shm`, and `/data/artifacts`; 3. start the container again. ## Restore Restore procedure: 1. stop the app container; 2. restore `db.sqlite`; 3. if the restored database came from SQLite `.backup`, remove any stale `db.sqlite-wal` and `db.sqlite-shm` files before starting; 4. restore `/data/artifacts`; 5. start the container; 6. verify `GET /healthz`, admin login, and at least one authenticated client download. Example: ```bash docker compose stop app cp /srv/update-server/backups/20260415-120000/db.sqlite /srv/update-server/data/db.sqlite rm -f /srv/update-server/data/db.sqlite-wal /srv/update-server/data/db.sqlite-shm tar -C /srv/update-server/data -xzf /srv/update-server/backups/20260415-120000/artifacts.tar.gz docker compose up -d app ``` ## Post-deploy checklist - `GET /healthz` returns `200`; - admin login works through HTTPS; - `Set-Cookie` on `/admin/login` includes `Secure`, `HttpOnly`, and the `/admin` path; - one release upload succeeds; - one bearer-authenticated `/api/v1/projects` request succeeds; - one `/api/v1/releases/{id}/download` request succeeds through the proxy; - backups complete and can be restored on a staging copy.