From 811d5f84cb100d8229e73a8b3b56ef3875d9f2e2 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Tue, 29 Sep 2026 12:37:06 -0400 Subject: [PATCH] Wiz-War's host follows the hall's shared layout: its own Caddy site file, its own access log, a memory cap for a shared box The host's Caddyfile gathers each game's site from /etc/caddy/sites/; Wiz-War's is rendered from deploy/Caddyfile.tmpl and logs to wizwar-access.log, which the rollup and the pulse read. The server's memory cap drops from 700M to 350M (it runs in about 125M), so a runaway cannot starve other games sharing the box. Applied on the live droplet with its logs renamed in place. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG --- .claude/skills/wizwar-pulse/SKILL.md | 4 ++-- deploy/Caddyfile.tmpl | 27 +++++++++++++++++++++ deploy/README.md | 5 ++-- deploy/setup-droplet.sh | 36 ++++++++++++++++++++-------- deploy/wizwar-rollup.sh | 2 +- deploy/wizwar.service | 5 ++-- 6 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 deploy/Caddyfile.tmpl diff --git a/.claude/skills/wizwar-pulse/SKILL.md b/.claude/skills/wizwar-pulse/SKILL.md index 7979308..860e22e 100644 --- a/.claude/skills/wizwar-pulse/SKILL.md +++ b/.claude/skills/wizwar-pulse/SKILL.md @@ -57,7 +57,7 @@ quiet, say so in one line before the details. lists - Unanswered feedback count from /var/lib/wizwar/feedback.jsonl — if any, offer to run the reports desk (`/wizwar-reports`) - - Access-log pulse: `wc -l /var/lib/caddy/access.log` and a count of + - Access-log pulse: `wc -l /var/lib/caddy/wizwar-access.log` and a count of distinct `client_ip`s in the last day, for the growth line ## Report @@ -77,7 +77,7 @@ action, or "nothing needs you." stale and leaves the rest asleep, so a deploy costs no memory spike. - Unattended-upgrades reboots the box at 09:30 UTC when a kernel patch requires it; a reboot there is maintenance, not an outage. -- Caddy access logs live at /var/lib/caddy/access.log (self-rotating, +- Caddy access logs live at /var/lib/caddy/wizwar-access.log (self-rotating, 10MiB × 30); the systemd sandbox denies /var/log/caddy. - Per-address limits (2026-09-03): 12 new rooms and 6 reports per address per hour, in packages/server/src/ratelimit.ts. A player who diff --git a/deploy/Caddyfile.tmpl b/deploy/Caddyfile.tmpl new file mode 100644 index 0000000..ea864e0 --- /dev/null +++ b/deploy/Caddyfile.tmpl @@ -0,0 +1,27 @@ +# Wiz-War's site on a host shared with the other games of Kestrel's Hall: +# setup-droplet.sh fills __HOST__ and writes it to /etc/caddy/sites/wizwar.caddy. +__HOST__ { + header { + Strict-Transport-Security "max-age=31536000" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "no-referrer" + } + + # Kept to 30 rolls of 10MiB: the nightly rollup keeps the counts; the raw + # lines back it for a while. Each game on the host keeps its own log. + log { + output file /var/lib/caddy/wizwar-access.log { + roll_size 10MiB + roll_keep 30 + } + } + + # A deploy restarts the game for a few seconds; the proxy holds requests + # that land in that gap and keeps dialing, so a visitor waits instead of + # meeting a 502. + reverse_proxy localhost:8787 { + lb_try_duration 30s + lb_try_interval 250ms + } +} diff --git a/deploy/README.md b/deploy/README.md index 1f748ef..55f44d7 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -35,8 +35,9 @@ longer replays becomes unreachable after restart, so this is not optional. 1. `doctl compute droplet create wizwar --region nyc3 --size s-1vcpu-1gb \ --image ubuntu-24-04-x64 --ssh-keys --tag-name wizwar --wait` -2. `scp deploy/setup-droplet.sh root@:/root/ && ssh root@ \ - "bash /root/setup-droplet.sh wizwar..sslip.io"` +2. `scp deploy/setup-droplet.sh deploy/Caddyfile.tmpl root@:/root/ && ssh root@ \ + "bash /root/setup-droplet.sh 'wizwar..sslip.io'"` (the host's Caddyfile + gathers each game's site from /etc/caddy/sites/, so the host can be shared) 3. `deploy/deploy.sh ` ## Operations diff --git a/deploy/setup-droplet.sh b/deploy/setup-droplet.sh index 5c268df..574dc8d 100755 --- a/deploy/setup-droplet.sh +++ b/deploy/setup-droplet.sh @@ -1,8 +1,12 @@ #!/usr/bin/env bash -# One-time droplet setup. Run ON the droplet as root: -# bash setup-droplet.sh wizwar..sslip.io +# One-time host setup. The host may be shared with the other games of +# Kestrel's Hall; this touches nothing of theirs. Copy it and Caddyfile.tmpl +# to the host and run ON it as root: +# bash setup-droplet.sh 'wizwar.kestrelsnest.social, wizwar..sslip.io' +# The argument is Wiz-War's Caddy site address line: one name, or several +# separated by commas. Every name must already resolve to this host. set -euo pipefail -HOST="${1:?usage: setup-droplet.sh }" +HOST="${1:?usage: setup-droplet.sh }" apt-get update -q apt-get install -qy curl git rsync @@ -24,13 +28,25 @@ id -u wizwar &>/dev/null || useradd -r -m -d /opt/wizwar-home wizwar mkdir -p /opt/wizwar /var/lib/wizwar/rooms chown -R wizwar:wizwar /opt/wizwar /var/lib/wizwar -# Caddy vhost: auto-TLS, security headers, proxy to the game. -# Access log kept to 30 rolls of 10MiB: the nightly rollup keeps the -# counts; the raw lines back it for a while. A deploy restarts the game -# for a few seconds; the proxy holds requests that land in that gap and -# keeps dialing, so a visitor waits instead of meeting a 502. -printf '%s\n\nheader {\n\tStrict-Transport-Security "max-age=31536000"\n\tX-Content-Type-Options "nosniff"\n\tX-Frame-Options "DENY"\n\tReferrer-Policy "no-referrer"\n}\nlog {\n\toutput file /var/lib/caddy/access.log {\n\t\troll_size 10MiB\n\t\troll_keep 30\n\t}\n}\nreverse_proxy localhost:8787 {\n\tlb_try_duration 30s\n\tlb_try_interval 250ms\n}\n' "$HOST" > /etc/caddy/Caddyfile -systemctl reload caddy +# Caddy: the host's Caddyfile only gathers each game's site file, so this +# host can be shared with the other games of Kestrel's Hall. A Caddyfile +# that is neither Caddy's stock one nor that is a single-site host from +# before the shared layout: stop rather than overwrite it. +ROOT_CADDY='# Kestrel'"'"'s Hall: every game on this host brings its own site file. +import sites/*.caddy' +mkdir -p /etc/caddy/sites +if ! grep -q '^import sites/\*.caddy' /etc/caddy/Caddyfile 2>/dev/null; then + if [ -s /etc/caddy/Caddyfile ] && ! grep -q 'root \* /usr/share/caddy' /etc/caddy/Caddyfile; then + echo "/etc/caddy/Caddyfile holds another site; move it into /etc/caddy/sites/ first" >&2 + exit 1 + fi + printf '%s\n' "$ROOT_CADDY" > /etc/caddy/Caddyfile +fi +sed "s|__HOST__|$HOST|" "$(dirname "$0")/Caddyfile.tmpl" > /etc/caddy/sites/wizwar.caddy +caddy validate --config /etc/caddy/Caddyfile +# Validation as root touches the access logs; Caddy runs as caddy and must own them. +chown -R caddy:caddy /var/lib/caddy +systemctl reload-or-restart caddy # Firewall: ssh + web only. The game server binds loopback and is reached # through Caddy; nothing else should answer the internet. diff --git a/deploy/wizwar-rollup.sh b/deploy/wizwar-rollup.sh index e5e62d6..6692fe8 100755 --- a/deploy/wizwar-rollup.sh +++ b/deploy/wizwar-rollup.sh @@ -43,7 +43,7 @@ paths = collections.Counter(); status = collections.Counter(); refs = collection # Where a visit came from, when the link itself says so: ?ref= on a # link Eric posted, a Facebook click's fbclid, or a utm_source. campaigns = collections.Counter(); seen_campaign = set() -for f in sorted(glob.glob("/var/lib/caddy/access*.log*")): +for f in sorted(glob.glob("/var/lib/caddy/wizwar-access*.log*")): if os.path.getmtime(f) < t0: continue opener = gzip.open if f.endswith(".gz") else open with opener(f, "rt", errors="ignore") as fh: diff --git a/deploy/wizwar.service b/deploy/wizwar.service index 4ebf397..cd88e51 100644 --- a/deploy/wizwar.service +++ b/deploy/wizwar.service @@ -27,8 +27,9 @@ ProtectKernelTunables=yes ProtectKernelModules=yes ProtectControlGroups=yes RestrictSUIDSGID=yes -# A runaway process gets killed and restarted before it can take the box down. -MemoryMax=700M +# A runaway process gets killed and restarted before it can take the box, or +# the other games sharing it, down. The server runs in about 125M. +MemoryMax=350M LimitNOFILE=4096 [Install]