From 605f8c511be8c916631f570378c9abe69f41c421 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Tue, 22 Sep 2026 17:15:58 -0400 Subject: [PATCH] Caddy routes the duel server's paths with handle blocks, from one template try_files ran before reverse_proxy and rewrote /api to the shell; handle blocks are exclusive. Both setup scripts render deploy/Caddyfile.tmpl. Co-Authored-By: Claude Fable 5.1 --- deploy/Caddyfile.tmpl | 42 +++++++++++++++++++++++++++++++++++++++++ deploy/README.md | 4 ++-- deploy/setup-droplet.sh | 39 +++++--------------------------------- deploy/setup-server.sh | 32 ++++++++++--------------------- 4 files changed, 59 insertions(+), 58 deletions(-) create mode 100644 deploy/Caddyfile.tmpl diff --git a/deploy/Caddyfile.tmpl b/deploy/Caddyfile.tmpl new file mode 100644 index 0000000..d24d919 --- /dev/null +++ b/deploy/Caddyfile.tmpl @@ -0,0 +1,42 @@ +__HOST__ + +root * /opt/waving-hands/build +encode gzip zstd + +header { + Strict-Transport-Security "max-age=31536000" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "no-referrer" +} + +log { + output file /var/lib/caddy/access.log { + roll_size 10MiB + roll_keep 30 + } +} + +# The duel server answers the API and the websocket; a deploy restarts it for +# a few seconds, and the proxy holds requests that land in that gap. +@duel path /api/* /ws +handle @duel { + reverse_proxy localhost:8788 { + lb_try_duration 30s + lb_try_interval 250ms + } +} + +# Everything else is the static site. SvelteKit writes each route as +# .html, so /rules is tried as /rules.html before falling back to the +# app shell, which serves the rooms. Hashed assets under _app/immutable are +# cached for a year; every other response is revalidated so a deploy shows +# up on the next load. The two header matchers are disjoint. +handle { + @immutable path /_app/immutable/* + header @immutable Cache-Control "public, max-age=31536000, immutable" + @mutable not path /_app/immutable/* + header @mutable Cache-Control "no-cache" + try_files {path} {path}.html /index.html + file_server +} diff --git a/deploy/README.md b/deploy/README.md index 9800cc4..5f325fe 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -33,10 +33,10 @@ uptime and upgrades independent of anything else. 1. `doctl compute droplet create waving-hands --region nyc3 \ --size s-1vcpu-512mb-10gb --image ubuntu-24-04-x64 \ --ssh-keys --tag-name waving-hands --wait` -2. `scp deploy/setup-droplet.sh root@:/root/ && ssh root@ \ +2. `scp deploy/setup-droplet.sh deploy/Caddyfile.tmpl root@:/root/ && ssh root@ \ "bash /root/setup-droplet.sh 'hands.kestrelsnest.social, waving-hands..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 root@:/root/ && ssh root@ "bash /root/setup-server.sh"` +3. `scp deploy/setup-server.sh deploy/Caddyfile.tmpl root@:/root/ && ssh root@ "bash /root/setup-server.sh"` 4. `deploy/deploy.sh ` The sslip.io hostname works with no DNS at all. To add a real name, point an diff --git a/deploy/setup-droplet.sh b/deploy/setup-droplet.sh index 594178e..671dde3 100755 --- a/deploy/setup-droplet.sh +++ b/deploy/setup-droplet.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# One-time droplet setup. Run ON the droplet as root: +# One-time droplet setup. Copy this script and Caddyfile.tmpl to the droplet +# and run ON the droplet as root: # bash setup-droplet.sh 'hands.kestrelsnest.social, waving-hands..sslip.io' # The argument is the Caddy site address line: one name, or several # separated by commas. Every name must already resolve to this droplet. @@ -24,39 +25,9 @@ mkdir -p /opt/waving-hands/build chown -R root:caddy /opt/waving-hands chmod -R g+rX /opt/waving-hands -# Caddy vhost: auto-TLS, security headers, static files. SvelteKit writes -# each route as .html, so /rules is tried as /rules.html before -# falling back to the app shell. Hashed assets under _app/immutable are -# cached for a year; every other response is revalidated so a deploy shows -# up on the next load. The two header matchers are disjoint, so their order -# does not matter. -cat > /etc/caddy/Caddyfile < /etc/caddy/Caddyfile +caddy validate --config /etc/caddy/Caddyfile systemctl reload caddy # Firewall: ssh + web only. diff --git a/deploy/setup-server.sh b/deploy/setup-server.sh index 8ec2dc5..d1269e5 100755 --- a/deploy/setup-server.sh +++ b/deploy/setup-server.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # Adds the duel server to a droplet that setup-droplet.sh already prepared. -# Run ON the droplet as root; safe to run again. Installs Node 22, creates -# the service user and data directory, and teaches Caddy to hand /api and -# /ws to the server while it keeps serving the static site itself. +# Copy this script and Caddyfile.tmpl to the droplet and run ON the droplet +# as root; safe to run again. Installs Node 22, creates the service user and +# data directory, and teaches Caddy to hand /api and /ws to the server while +# it keeps serving the static site itself. set -euo pipefail if ! command -v node >/dev/null || [[ "$(node -v)" != v22* ]]; then @@ -14,24 +15,11 @@ id -u waving-hands &>/dev/null || useradd -r -m -d /opt/waving-hands-home waving mkdir -p /opt/waving-hands/app /var/lib/waving-hands/rooms chown -R waving-hands:waving-hands /var/lib/waving-hands -# Caddy: the two proxied paths go in before the static file handling. -if ! grep -q 'reverse_proxy' /etc/caddy/Caddyfile; then - python3 - <<'PY' -import pathlib -p = pathlib.Path('/etc/caddy/Caddyfile'); s = p.read_text() -marker = 'try_files {path} {path}.html /index.html' -proxy = '''@duel path /api/* /ws -reverse_proxy @duel localhost:8788 { - lb_try_duration 30s - lb_try_interval 250ms -} - -''' -assert marker in s -p.write_text(s.replace(marker, proxy + marker)) -PY - caddy validate --config /etc/caddy/Caddyfile - systemctl reload caddy -fi +# Caddy: rewrite the site's configuration from the template, keeping its +# address line, so the duel server's paths are routed before the files. +HOST_LINE=$(head -1 /etc/caddy/Caddyfile) +sed "s|__HOST__|$HOST_LINE|" "$(dirname "$0")/Caddyfile.tmpl" > /etc/caddy/Caddyfile +caddy validate --config /etc/caddy/Caddyfile +systemctl reload caddy echo "server prerequisites ready: now run deploy/deploy.sh from your machine"