No description
  • Shell 60.9%
  • Python 20.5%
  • HTML 18.6%
Find a file
Zach Town 707d392d5b Place grimhang's rows instead of letting them fall alphabetical
The grimhang card read hang, last session, pull-up, sessions -- the date wedged
between the two lifts, because an unlisted key sorts alphabetically and "last
session" starts with an l.

That ordering is a property of the page, not of the service, so the fix belongs
here rather than in a key renamed to sort right from the other end. ORDER now
lists the four keys grimhang publishes, in the order its author meant: what the
service is, then when it was last used, with the page's own "checked" row still
last.

Nothing else moves. The other four cards share none of these keys, and appending
to ORDER cannot reorder keys that were already listed -- checked against all five
live cards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 14:40:08 -06:00
00-user vps bootstrap scripts initial commit 2026-05-22 09:17:13 -06:00
01-caddy Migrate from nginx + certbot to Caddy 2026-09-24 09:21:11 -06:00
02-website Place grimhang's rows instead of letting them fall alphabetical 2026-10-06 14:40:08 -06:00
04-forgejo Migrate from nginx + certbot to Caddy 2026-09-24 09:21:11 -06:00
05-claude-access Migrate from nginx + certbot to Caddy 2026-09-24 09:21:11 -06:00
06-apps Refresh the setup checklist and drop the stale app template on re-run 2026-09-24 09:25:56 -06:00
07-metrics Fix vps-metrics CPU read failing on long-uptime boxes 2026-09-24 09:21:20 -06:00
08-deploy-webhook Report the deployed commit, and stop flagging deploy cards as stale 2026-09-24 10:55:11 -06:00
.gitignore Add push-triggered deploy webhook 2026-09-18 22:01:54 -06:00
CLAUDE.md Refresh the setup checklist and drop the stale app template on re-run 2026-09-24 09:25:56 -06:00
LICENSE Initial commit 2026-05-22 15:00:10 +00:00
README.md Report the deployed commit, and stop flagging deploy cards as stale 2026-09-24 10:55:11 -06:00

vps-bootstrap

Scripts to bootstrap a fresh Debian VPS.

Order of operations

Step Directory What it does
0 00-user/ Install sudo, create zach user, harden SSH — run as root
1 01-caddy/ Install UFW + Caddy (official repo), open ports 22 and 80/443
2 02-website/ Create web root, install Caddy config for zach.town
3 04-forgejo/caddy-setup.sh Install Caddy config for git.zach.town
4 04-forgejo/setup.sh Install Forgejo binary + systemd service
5 04-forgejo/auto-update-setup.sh Install the daily Forgejo auto-updater (same-major releases only)
6 05-claude-access/ Scoped passwordless sudo (Caddy validate/reload only) + enable lingering, for Claude Code deploy access
7 06-apps/ Install the ~/apps/ convention + templates for running apps as systemd --user services
8 07-metrics/ Install the "vps" card on the status page: CPU/memory/disk/load, refreshed every minute
9 08-deploy-webhook/ Install the loopback webhook receiver that auto-deploys an app on git push (git pull + optional build + systemd --user restart)

Keeping things up to date

What How Where the logic lives
Caddy, OS packages Debian unattended-upgrades (security updates nightly). Caddy comes from the official repo, whose origin 01-caddy/setup.sh adds to the unattended-upgrades origins list so it's patched on the same footing. 01-caddy/setup.sh
Forgejo forgejo-update.timer, daily 04:30 UTC. Applies the newest release within the installed major after sha256 + GPG verification, keeps the previous binary and a SQLite copy, rolls back if the new one doesn't start. A new major is only flagged on the status page; apply it by hand with sudo bash /usr/local/lib/forgejo-update/update.sh --allow-major after reading the release notes. 04-forgejo/update.sh + lib.sh
CS2 server Its own timers, in the cs2-server repo that repo
Status page https://zach.town/status/ (linked from the site's index) shows every updater's last check, versions, live CS2 player count, and anything waiting on you. Updaters call vps-status-write <service> key=value…, which writes JSON to /var/www/zach.town/status/data/; Caddy's file_server browse serves that directory listing as JSON (it content-negotiates on Accept: application/json) so the page needs no backend. 02-website/status/
VPS system load vps-metrics.timer, every minute. CPU/mem/disk/load, no update mechanism (informational only). 07-metrics/report-metrics.sh
systemctl list-timers forgejo-update.timer 'cs2-*'
journalctl -u forgejo-update -n 50
sudo bash /usr/local/lib/forgejo-update/update.sh --check

Publishing something new on the status page

Any key the page doesn't recognise is rendered as a plain row, so adding info needs no page changes:

vps-status-write myservice state=ok installed=1.4.0 queue="12 jobs"
vps-status-write --keep-checked-at myservice players="3 / 10" live_at=now
vps-status-write myservice latest=          # empty value removes a key

state drives the badge (ok, update-available, updating, skipped, manual-required, error), message adds a line under the rows, and stale_after (seconds) is how long without a checked_at refresh before the page flags the service as stale — or never, for a reporter that's event-driven rather than on a timer. A deploy hook only writes when someone pushes, so a quiet fortnight is its normal resting state; without never it would sit permanently red claiming its timer had stopped. Use --keep-checked-at for side-channel writers that shouldn't reset that clock — a per-minute player count reporter must not make a wedged update checker look healthy. The page takes its colours from the site's /style.css.

Any value that's a plain integer 0-100 (e.g. cpu=42) renders as a heat-gradient bar instead of text — no page changes needed there either. vps-metrics uses this for CPU/mem/disk; reuse it for anything else that's naturally a percentage.

Auto-deploying an app on push

08-deploy-webhook/ installs a loopback-only receiver that Forgejo calls on git push, which does git pull + an optional build command + systemctl --user restart for the matching app — push-to-deploy without a CI runner or a "remember to git pull" step. See that directory's README.md for how to register an app.

Notes

  • Step 0 runs as root. All subsequent steps run as zach with sudo.
  • Pass your public key to step 0 so you're not locked out after password auth is disabled: ssh root@<ip> 'bash -s' < 00-user/setup.sh < ~/.ssh/id_ed25519.pub or pipe it: cat ~/.ssh/id_ed25519.pub | ssh root@<ip> 'bash /path/to/setup.sh'
  • TLS is automatic and needs no step of its own: Caddy obtains and renews a Let's Encrypt certificate for every hostname in /etc/caddy/sites-enabled/*.caddy and redirects HTTP to HTTPS. Certs live in /var/lib/caddy/.local/share/caddy/. Unlike certbot, Caddy never rewrites these config files — what's checked in here is what runs on the box.
  • 04-forgejo/setup.sh installs the newest stable release; set FORGEJO_VERSION=X.Y.Z in the environment to pin one.
  • After Forgejo starts, complete initial setup via the web UI at http://git.zach.town.
  • 02-website/setup.sh also installs the status page; on an existing box, 02-website/status-setup.sh alone adds it without touching the site.
  • Steps 7-8 assume an app-hosting user (zach) that can run sudo interactively at least once, to install the scoped sudoers file and enable lingering.
  • 06-apps/PORTS.md is a template only — the live registry on the VPS (~/apps/PORTS.md) is not tracked here and won't be overwritten by re-running 06-apps/setup.sh.
  • 08-deploy-webhook/config.json.template is likewise a template only — the live config (~/apps/deploy-webhook/config.json) holds secret deploy tokens and is never overwritten by re-running 08-deploy-webhook/setup.sh.
  • No secrets belong in this repo (it's public). API tokens, .env files, and TLS keys stay off the VPS filesystem outside their expected locations and are never committed here.