- Shell 60.9%
- Python 20.5%
- HTML 18.6%
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> |
||
|---|---|---|
| 00-user | ||
| 01-caddy | ||
| 02-website | ||
| 04-forgejo | ||
| 05-claude-access | ||
| 06-apps | ||
| 07-metrics | ||
| 08-deploy-webhook | ||
| .gitignore | ||
| CLAUDE.md | ||
| LICENSE | ||
| README.md | ||
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 aszachwithsudo. - 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.pubor 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/*.caddyand 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.shinstalls the newest stable release; setFORGEJO_VERSION=X.Y.Zin the environment to pin one.- After Forgejo starts, complete initial setup via the web UI at
http://git.zach.town. 02-website/setup.shalso installs the status page; on an existing box,02-website/status-setup.shalone adds it without touching the site.- Steps 7-8 assume an app-hosting user (
zach) that can runsudointeractively at least once, to install the scoped sudoers file and enable lingering. 06-apps/PORTS.mdis a template only — the live registry on the VPS (~/apps/PORTS.md) is not tracked here and won't be overwritten by re-running06-apps/setup.sh.08-deploy-webhook/config.json.templateis likewise a template only — the live config (~/apps/deploy-webhook/config.json) holds secret deploy tokens and is never overwritten by re-running08-deploy-webhook/setup.sh.- No secrets belong in this repo (it's public). API tokens,
.envfiles, and TLS keys stay off the VPS filesystem outside their expected locations and are never committed here.