160 lines
5.2 KiB
Markdown
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.
|