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

160 lines
5.2 KiB
Markdown

# 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.