--- name: deploy-drinktracker description: Deploy drinktracker to the production LXC and verify it came back healthy. Use when asked to deploy, ship, release, push to prod, or update the running app - and after merging changes that need to go live. Also covers rollback and diagnosing a deploy that did not take effect. --- # Deploying drinktracker Run `./deploy/deploy.sh` from the repo root (`--yes` to skip the prompt when the user has already approved). It typechecks, pushes to Gitea, builds natively on the LXC into `releases/`, swaps the `current` symlink, restarts, and polls until healthy. `./deploy/deploy.sh --rollback` repoints at the previous release in ~3 seconds with no rebuild. `deploy/README.md` has the full topology, backup/restore, and manual verification commands. **The app runs natively under systemd. There is no Docker on this host.** ## Facts not discoverable from the repo - **The LXC is `192.168.2.169`.** `drinktracker.tenseconddelay.net` and `NEXTAUTH_URL` resolve to the reverse proxy at `192.168.2.172`. SSHing there fails in a way that looks like a rejected key but is a different machine entirely. - **Access:** `ssh -i ~/.ssh/drinktracker_ed25519 drinkadmin@192.168.2.169`. Root SSH is disabled. Confirm what's live with `readlink /opt/drinktracker/current`. - **Layout:** app `/opt/drinktracker/{repo,releases/,current}`, service user `drinktracker`, secrets `/etc/drinktracker/drinktracker.env`, units `drinktracker`, `minio`, `postgresql@16-main`. - **Database is `drinkman`, user `drinkdb`** — not `drinktracker`/`drinktracker`. - **No CI.** `git push` updates the Gitea repo and nothing else. If someone reports "I pushed but nothing changed", this is why. ## Three things that will bite you **`next.config.mjs` evaluates `rewrites()` at build time.** The `/minio-images/:path*` destination is baked into `routes-manifest.json` as `http://localhost:9000/drink-images/:path*`. Changing `MINIO_ENDPOINT` in the env file looks like it works and does nothing; images break only after the next rebuild. MinIO must stay on `localhost:9000`. For the same reason the deploy builds *without* sourcing the env file, exactly as the Dockerfile does — don't "fix" that. **`prisma db push`, never `migrate deploy`.** Production has no `_prisma_migrations` table, so `migrate deploy` would try to apply `20260228220749_init` against 16 populated tables and fail. This is the obvious-looking modernization that breaks production. **`--accept-data-loss` drops columns.** Removing a field from `schema.prisma` deletes the column and its data on the next deploy, in both directions — so rolling back past a schema change is destructive too. The deploy script dumps to `/var/backups/drinktracker/pre-.sql.gz` first. This is also why `UserPreference.defaultProvider` is still in the schema despite being unused. Manual dump: `sudo -u postgres pg_dump drinkman > ~/dt-$(date +%F).sql` ## Verifying An unauthenticated request to port 3000 returns **307** (redirect to `/login`) — healthy, not a failure. The script accepts 200, 302 and 307. ``` sudo journalctl -u drinktracker | grep '\[switchboard\]' ``` Each AI call emits one line with the model and cost. `FAILOVER` lines are expected and harmless: the gateway's `:batch` model variants fail on every request and fall back. `CONTEXT OVERFLOW` means a request exceeded every model's window and may have been truncated — worth investigating. ## If the app does not come back ``` systemctl status drinktracker sudo journalctl -u drinktracker -n 100 systemctl is-active postgresql@16-main minio readlink /opt/drinktracker/current ``` Most likely, in order: the build failed and the symlink still points at the old release (so the app is fine, the deploy just didn't take); `/etc/drinktracker/drinktracker.env` is missing a variable the new code reads (compare against `.env.example`); or postgres/minio didn't start, which `is-active` shows immediately. Note the systemd unit deliberately omits `MemoryDenyWriteExecute` (it breaks the V8 JIT) and includes `AF_NETLINK` in `RestrictAddressFamilies` (Next needs it at startup). If you harden that unit further and the app stops booting, those are the two to suspect. ## Docker `Dockerfile` and `docker-compose.prod.yml` are kept for running this app on a VPS and still work on a normal Docker host. They cannot work on this LXC: unprivileged-LXC nesting breaks AppArmor profile loading and the `net.ipv4.ip_unprivileged_port_start` sysctl, and `docker build` is impossible since Docker 29 removed the classic builder. Docker is installed but disabled; old containers and volumes are retained until 2026-09-07 as a rollback path.