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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d5b92d9a63
commit
811d5f84cb
@@ -57,7 +57,7 @@ quiet, say so in one line before the details.
|
|||||||
lists
|
lists
|
||||||
- Unanswered feedback count from /var/lib/wizwar/feedback.jsonl —
|
- Unanswered feedback count from /var/lib/wizwar/feedback.jsonl —
|
||||||
if any, offer to run the reports desk (`/wizwar-reports`)
|
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
|
distinct `client_ip`s in the last day, for the growth line
|
||||||
|
|
||||||
## Report
|
## Report
|
||||||
@@ -77,7 +77,7 @@ action, or "nothing needs you."
|
|||||||
stale and leaves the rest asleep, so a deploy costs no memory spike.
|
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
|
- Unattended-upgrades reboots the box at 09:30 UTC when a kernel patch
|
||||||
requires it; a reboot there is maintenance, not an outage.
|
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.
|
10MiB × 30); the systemd sandbox denies /var/log/caddy.
|
||||||
- Per-address limits (2026-09-03): 12 new rooms and 6 reports per
|
- 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
|
address per hour, in packages/server/src/ratelimit.ts. A player who
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-2
@@ -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 \
|
1. `doctl compute droplet create wizwar --region nyc3 --size s-1vcpu-1gb \
|
||||||
--image ubuntu-24-04-x64 --ssh-keys <your-key-ids> --tag-name wizwar --wait`
|
--image ubuntu-24-04-x64 --ssh-keys <your-key-ids> --tag-name wizwar --wait`
|
||||||
2. `scp deploy/setup-droplet.sh root@<ip>:/root/ && ssh root@<ip> \
|
2. `scp deploy/setup-droplet.sh deploy/Caddyfile.tmpl root@<ip>:/root/ && ssh root@<ip> \
|
||||||
"bash /root/setup-droplet.sh wizwar.<ip>.sslip.io"`
|
"bash /root/setup-droplet.sh 'wizwar.<ip>.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 <ip>`
|
3. `deploy/deploy.sh <ip>`
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
|
|||||||
+26
-10
@@ -1,8 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# One-time droplet setup. Run ON the droplet as root:
|
# One-time host setup. The host may be shared with the other games of
|
||||||
# bash setup-droplet.sh wizwar.<droplet-ip>.sslip.io
|
# 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.<host-ip>.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
|
set -euo pipefail
|
||||||
HOST="${1:?usage: setup-droplet.sh <hostname>}"
|
HOST="${1:?usage: setup-droplet.sh <site address line>}"
|
||||||
|
|
||||||
apt-get update -q
|
apt-get update -q
|
||||||
apt-get install -qy curl git rsync
|
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
|
mkdir -p /opt/wizwar /var/lib/wizwar/rooms
|
||||||
chown -R wizwar:wizwar /opt/wizwar /var/lib/wizwar
|
chown -R wizwar:wizwar /opt/wizwar /var/lib/wizwar
|
||||||
|
|
||||||
# Caddy vhost: auto-TLS, security headers, proxy to the game.
|
# Caddy: the host's Caddyfile only gathers each game's site file, so this
|
||||||
# Access log kept to 30 rolls of 10MiB: the nightly rollup keeps the
|
# host can be shared with the other games of Kestrel's Hall. A Caddyfile
|
||||||
# counts; the raw lines back it for a while. A deploy restarts the game
|
# that is neither Caddy's stock one nor that is a single-site host from
|
||||||
# for a few seconds; the proxy holds requests that land in that gap and
|
# before the shared layout: stop rather than overwrite it.
|
||||||
# keeps dialing, so a visitor waits instead of meeting a 502.
|
ROOT_CADDY='# Kestrel'"'"'s Hall: every game on this host brings its own site file.
|
||||||
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
|
import sites/*.caddy'
|
||||||
systemctl reload 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
|
# Firewall: ssh + web only. The game server binds loopback and is reached
|
||||||
# through Caddy; nothing else should answer the internet.
|
# through Caddy; nothing else should answer the internet.
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ paths = collections.Counter(); status = collections.Counter(); refs = collection
|
|||||||
# Where a visit came from, when the link itself says so: ?ref=<name> on a
|
# Where a visit came from, when the link itself says so: ?ref=<name> on a
|
||||||
# link Eric posted, a Facebook click's fbclid, or a utm_source.
|
# link Eric posted, a Facebook click's fbclid, or a utm_source.
|
||||||
campaigns = collections.Counter(); seen_campaign = set()
|
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
|
if os.path.getmtime(f) < t0: continue
|
||||||
opener = gzip.open if f.endswith(".gz") else open
|
opener = gzip.open if f.endswith(".gz") else open
|
||||||
with opener(f, "rt", errors="ignore") as fh:
|
with opener(f, "rt", errors="ignore") as fh:
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ ProtectKernelTunables=yes
|
|||||||
ProtectKernelModules=yes
|
ProtectKernelModules=yes
|
||||||
ProtectControlGroups=yes
|
ProtectControlGroups=yes
|
||||||
RestrictSUIDSGID=yes
|
RestrictSUIDSGID=yes
|
||||||
# A runaway process gets killed and restarted before it can take the box down.
|
# A runaway process gets killed and restarted before it can take the box, or
|
||||||
MemoryMax=700M
|
# the other games sharing it, down. The server runs in about 125M.
|
||||||
|
MemoryMax=350M
|
||||||
LimitNOFILE=4096
|
LimitNOFILE=4096
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
Reference in New Issue
Block a user