Files
game-kit/template/deploy
Eric WagonerandClaude Fable 5.1 33ac0ec808 The credibility pass on Hnefatafl, ported to the template: the ops scripts count traffic through one parser, the reports digest is one program, the plaintext-token fallback and its migration are gone, and the stylesheet holds each shared rule once
From the hnefatafl repo's pass of the same day, everything that touched a
kit-shared file. The visitors digest and the nightly rollup share
deploy/traffic.py, installed to /usr/local/lib/<slug>; the rollup writes
the finished-games count it computed behind "and False". pull-reports.sh
and the reports skill both use deploy/report-digest.ts. The seat line
requires its token hash and the start line its rules revision; the
migration for ledgers written before hashing goes with them. The route
table in server/src/index.ts lists every route; Report, ReportLine and
Tally are declared once in view.ts for both sides; exports nobody
imported are exports no more.

In the client: .small, the × that dismisses, and the frame of the
reading pages are in app.css once; the preferences panel shares the
report slip's modal shape; the room store gains seatEmpty and
seatUnheld, and the lobby and the join page read those instead of
three spellings of their own. The room's moved and awaiting fields
stay: the demo board reads them, and simultaneous rounds are the
contract.

The deploy README no longer describes a browser-only game; the visitors
skill no longer names a /play route; the reports skill's replay call
carries the room's options. The Slack channel id is passed in the
environment rather than filled in as a placeholder.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwFKMuQnPAEHJ5yA1q4orh
2026-09-23 20:12:37 -04:00
..

Deploying NAME

Every game, against a housecarl or a friend, is a room on a small Node process that keeps it as an append-only ledger of moves in /var/lib/SLUG/rooms. Production is one DigitalOcean droplet: Caddy terminates TLS with automatic certificates, serves the static build from /opt/SLUG/build, and proxies /api and /ws to the game server on port PORT, which runs as the __SLUG__ user under systemd from /opt/SLUG/app.

Sizing

The smallest droplet, s-1vcpu-512mb-10gb ($4 a month), carries both Caddy and the game server comfortably: the server is one small Node process capped at 300 MB by its unit file, and a room is a few kilobytes of ledger.

Current production

  • Droplet: __SLUG__ (nyc3, s-1vcpu-512mb-10gb, tag __SLUG__), IP IP
  • URLs: https://DOMAIN (A record at Hover, where kestrelsnest.social's DNS lives) and https://SLUG.IP.sslip.io (always works, zero DNS).
  • Everyday deploy: deploy/deploy.sh __IP__
  • Players' reports: deploy/pull-reports.sh mirrors /var/lib/SLUG/feedback.jsonl and the screenshots to ~/Desktop/SLUG-reports with a digest; deploy/report-reply.sh __IP__ <id> <status> "text" answers one.

New droplet from scratch

  1. doctl compute droplet create __SLUG__ --region nyc3 \ --size s-1vcpu-512mb-10gb --image ubuntu-24-04-x64 \ --ssh-keys <your-key-ids> --tag-name __SLUG__ --wait
  2. scp deploy/setup-droplet.sh deploy/Caddyfile.tmpl root@<ip>:/root/ && ssh root@<ip> \ "bash /root/setup-droplet.sh '__DOMAIN__, __SLUG__.<ip>.sslip.io'" (point the A record at the new IP first, or leave the real name out until it is).
  3. scp deploy/setup-server.sh deploy/Caddyfile.tmpl root@<ip>:/root/ && ssh root@<ip> "bash /root/setup-server.sh"
  4. deploy/deploy.sh <ip>

The sslip.io hostname works with no DNS at all. To add a real name, point an A record at the droplet and add the name to the first line of /etc/caddy/Caddyfile (space separated), then systemctl reload caddy; Caddy fetches the certificate on first request.

Operations scripts on the droplet

deploy.sh installs these to /usr/local/bin, the traffic.py they share to /usr/local/lib/SLUG, and the cron file to /etc/cron.d/SLUG on every deploy, so the live copies are the repo copies:

  • __SLUG__-visitors.sh [day]: who is here now and who came that day.
  • __SLUG__-pulse.sh: the weekly health check (service, errors, rollup trend, backup, box).
  • __SLUG__-rollup.sh [day]: one JSON line per day of counts, run nightly at 00:12 UTC into /var/lib/SLUG/rollup.jsonl.
  • __SLUG__-backup.sh: nightly at 07:23 UTC, mirrors /var/lib/SLUG to the kestrel-wizwar-backups Space under __SLUG__/ (a current copy and dated snapshots kept 90 days). It needs rclone with the Spaces credentials in /root/.config/rclone/rclone.conf, copied by hand from the wizwar droplet; until then it logs "skipped".

Errors from the game server go to Sentry, project __SLUG__ in the locallygrownnet organisation; the DSN is in the unit file. The nightly rollup and backup check in with Sentry Crons when /root/.SLUG-sentry-cron-rollup and /root/.SLUG-sentry-cron hold their check-in URLs (the ingest URL with the project's cron path and public key), so a missed night is noticed.

To have every new issue, regression and reappearance posted to Slack the way wizwar's are, run deploy/sentry-slack-alert.sh [#channel] once from your own shell with SENTRY_TOKEN (an org auth token with alerts:write) and SLACK_CHANNEL_ID (the channel's Slack ID) set; the token never leaves the shell.

Before every deploy, deploy/verify-ledgers.sh <ip> fetches every production ledger and replays it with the local engine, comparing each room with what the server shows. A ledger the new engine refuses or replays differently stops the deploy: the server would rewrite that game on restart.

Everyday deploys

deploy/deploy.sh <ip>

Runs the type-checks, the tests and the build locally, rsyncs build/ to the droplet keeping the previous week's hashed assets, rsyncs the server and engine sources, installs dependencies, and restarts the game server. A room is replayed from its ledger when the server comes back, which takes a few seconds; Caddy holds requests that land in the gap.

Operations

  • Logs: ssh root@<ip> journalctl -u __SLUG__ -f for the game server, journalctl -u caddy -f and /var/lib/caddy/access.log for the web side.
  • Restart: ssh root@<ip> systemctl restart __SLUG__
  • Rooms: /var/lib/__SLUG__/rooms/<CODE>.jsonl, one ledger per game; the nightly backup keeps them in the Space.