diff --git a/docs/caddy-updates.conf b/docs/caddy-updates.conf new file mode 100644 index 00000000..ffa9178c --- /dev/null +++ b/docs/caddy-updates.conf @@ -0,0 +1,71 @@ +# Caddy snippet for updates.jakubkadlec.dev. +# +# Drop this into the main Caddyfile on the VPS (or include it from there). +# Caddy already handles TLS via Let's Encrypt for the parent zone; this block +# only adds a subdomain that serves the update manifest, detached signature, +# and content-addressed blob store. +# +# Directory layout on disk (owned by the release operator, not Caddy): +# +# /var/www/updates.jakubkadlec.dev/ +# ├── manifest.json +# ├── manifest.json.sig +# ├── manifests/ +# │ └── 2026.04.14-1.json (archived historical manifests) +# ├── files/ +# │ └── / content-addressed blobs +# └── launcher/ Velopack feed (populated by Velopack's own publish tool) +# +# Create with: +# sudo mkdir -p /var/www/updates.jakubkadlec.dev/{files,manifests,launcher} +# sudo chown -R mt2.jakubkadlec.dev:mt2.jakubkadlec.dev /var/www/updates.jakubkadlec.dev +# +# Then add this to Caddy and `sudo systemctl reload caddy`. + +updates.jakubkadlec.dev { + root * /var/www/updates.jakubkadlec.dev + + # Allow clients to resume interrupted downloads via HTTP Range. + # Caddy's file_server sets Accept-Ranges: bytes by default, so there's + # nothing extra to configure for this — listed explicitly as a reminder. + file_server { + precompressed gzip br + } + + # Content-addressed blobs are immutable (the hash IS the file name), so we + # can tell clients to cache them forever. A manifest update never rewrites + # an existing blob. + @blobs path /files/* + header @blobs Cache-Control "public, max-age=31536000, immutable" + + # The manifest and its signature must never be cached beyond a minute — + # clients need to see new releases quickly, and stale caches would delay + # rollouts. Short TTL, not zero, to absorb thundering herds on release. + @manifest path /manifest.json /manifest.json.sig + header @manifest Cache-Control "public, max-age=60, must-revalidate" + + # Historical manifests are as immutable as blobs — named by version. + @archive path /manifests/* + header @archive Cache-Control "public, max-age=31536000, immutable" + + # The Velopack feed (launcher self-update) is a separate tree managed by + # Velopack's publishing tool. Same cache rules as the main manifest: short + # TTL on the feed metadata, blobs are immutable. + @velopack-feed path /launcher/RELEASES* + header @velopack-feed Cache-Control "public, max-age=60, must-revalidate" + + @velopack-blobs path /launcher/*.nupkg + header @velopack-blobs Cache-Control "public, max-age=31536000, immutable" + + # CORS is not needed — the launcher is a native app, not a browser — so + # no Access-Control-Allow-Origin header. If a web changelog page ever needs + # to fetch the manifest from the browser, revisit this. + + # Deny directory listings; the launcher knows exactly which paths it wants. + file_server browse off 2>/dev/null || file_server + + log { + output file /var/log/caddy/updates.jakubkadlec.dev.access.log + format json + } +}