Files
drinktracker/deploy/README.md
JP 45d9bd12d7 Add deploy script and skill for the production LXC
There is no CI, so a git push deploys nothing - the image has to be built
and the stack restarted separately. This captures that as one command
rather than a sequence to remember.

The image is built on the LXC and tagged with the registry name the
compose file already references, so compose finds it locally and never
pulls. That means no registry credentials are needed on either machine.

Also records the two things that cost the most time to work out: the
public hostname resolves to the reverse proxy rather than the container
(192.168.2.169), and the live stack runs from /root/drinktracker while
the checkouts under /home/drinkadmin are stale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 17:02:20 +00:00

3.6 KiB

Deploying drinktracker

./deploy/deploy.sh          # typecheck, push, build on the LXC, restart, verify
./deploy/deploy.sh --yes    # skip the confirmation prompt
./deploy/deploy.sh --push   # also publish the image to the Gitea registry

Where things actually live

Production LXC 192.168.2.169 (hostname drinktracker)
Deploy user drinkadmin, key ~/.ssh/drinktracker_ed25519
Live compose dir /root/drinktracker
Image tag 192.168.2.140:3000/jpscott84/drinktracker:latest
App port 3000 (host network), unauthenticated requests return 307 → /login

Two traps worth knowing:

drinktracker.tenseconddelay.net is the reverse proxy (192.168.2.172), not the container. NEXTAUTH_URL points at the public name, so resolving it and SSHing there fails confusingly — the key looks rejected because it's a different machine.

The two checkouts under /home/drinkadmin/ are stale. Only /root/drinktracker is live; that's what the running container's compose labels point to.

How it works

The image is built on the LXC and tagged with the registry name that docker-compose.prod.yml already references. Compose finds it locally and never pulls, so no registry credentials are needed on either machine. --push publishes it as well, which is only useful if something else pulls that tag.

There is no CI, so git push on its own deploys nothing — it only updates the Gitea repo. The build and restart have to happen separately, which is what this script is for.

One-time setup

The script needs passwordless sudo on the LXC, because the deployment lives under /root. On the container as root:

echo 'drinkadmin ALL=(root) NOPASSWD: ALL' > /etc/sudoers.d/drinkadmin-deploy
chmod 440 /etc/sudoers.d/drinkadmin-deploy
visudo -c

This grants less than it appears to: drinkadmin is already in the docker group, and anyone who can talk to the Docker socket can mount the host filesystem into a privileged container, so they are root-equivalent already. If you'd rather not, drop the sudoers file and run the deploy steps by hand from a root shell.

Alternative worth considering: move the deployment out of /root into a drinkadmin-owned directory and skip sudo entirely. Keep the directory basename drinktracker so the compose project name — and therefore the existing container and volume names — stay the same. .env.production must be copied across intact or Postgres will reject the app's credentials, since the volume was initialised with the current password.

Verifying by hand

ssh -i ~/.ssh/drinktracker_ed25519 drinkadmin@192.168.2.169
sudo docker compose -f /root/drinktracker/docker-compose.prod.yml ps
sudo docker logs drinktracker-app-1 --tail 50
sudo docker logs drinktracker-app-1 2>&1 | grep '\[switchboard\]'   # AI routing + cost per call

Each AI call logs one [switchboard] feature=… model=… cost=… latency_ms=… line. FAILOVER warnings are expected — the gateway's :batch model variants currently fail on every request and fall back.

Rollback

git revert <bad-sha> && ./deploy/deploy.sh

Or directly on the LXC, without touching git history:

sudo git -C /root/drinktracker reset --hard <previous-sha>
cd /root/drinktracker
sudo docker build -t 192.168.2.140:3000/jpscott84/drinktracker:latest .
sudo docker compose -f docker-compose.prod.yml up -d --no-build

Note the migrate service runs prisma db push --accept-data-loss on every start, so a rollback that removes a column will drop it and its data. Take a database dump first if the schema changed.