update_server/docs/DEPLOYMENT.md
2026-06-10 20:51:17 +03:00

5.2 KiB

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:
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:

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:

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.