docs: design the update manager + manifest generator #3

Merged
jakub merged 7 commits from jann/m2dev-client:claude/update-manager into main 2026-04-14 11:54:43 +02:00
Showing only changes of commit 02061f6e07 - Show all commits

71
docs/caddy-updates.conf Normal file
View File

@@ -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/
# │ └── <hash[0:2]>/<hash> 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
}
}