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