feat(docker): add opt-in bundled reverse-proxy profile (Caddy + lego)

Make the default safe-subdomains-fullreplay mode painless to deploy
without baking heavy proxy/cert deps into the archivebox image.

docker-compose.proxy.yml is an opt-in overlay that adds:
  - caddy: wildcard catch-all reverse proxy (etc/Caddyfile) that routes
    every dynamically-generated snapshot/role subdomain to archivebox by
    suffix wildcard (never an enumerated list), preserves Host, and sets
    X-Forwarded-Proto. Local default serves *.archivebox.localhost via
    Caddy's internal CA.
  - lego (proxy-public profile): a go-acme/lego DNS-01 sidecar
    (bin/proxy_lego.sh) that fetches ONE *.<domain> wildcard cert covering
    unlimited snap-* subdomains. Provider coverage (~150: cloudflare,
    route53, gcloud, digitalocean, hetzner, namecheap, godaddy, ...) is
    delegated to lego via standard env vars, with no per-provider code.

When proxied, the archivebox service stops publishing :8000 directly and
sets REVERSE_PROXY_TRUST_FORWARDED_PROTO=True so secure cookies + https
links work end-to-end. Replaces the stale nginx example in
docker-compose.yml. See .env.proxy.example for public setup.
This commit is contained in:
archivebox 2026-06-04 22:15:35 +00:00
parent a9cc7f887b
commit a7514a7eb8
No known key found for this signature in database
6 changed files with 256 additions and 12 deletions

42
.env.proxy.example Normal file
View File

@ -0,0 +1,42 @@
# Example env for the bundled reverse-proxy profile (docker-compose.proxy.yml).
# Copy to `.env` and edit for a PUBLIC deployment with automatic wildcard TLS.
# For LOCAL *.archivebox.localhost you don't need any of this.
# ---- Your public base domain (ArchiveBox lives at admin./web./api./public./snap-*.<this>) ----
ARCHIVEBOX_PROXY_DOMAIN=archive.example.com
# Tell Caddy to serve the single *.<domain> wildcard cert the lego sidecar fetches:
ARCHIVEBOX_PROXY_SITE=*.archive.example.com archive.example.com
ARCHIVEBOX_PROXY_TLS=/certs/wildcard.crt /certs/wildcard.key
# ---- DNS-01 issuance (no per-provider hand-coding; lego does ~150 providers) ----
ARCHIVEBOX_PROXY_DNS_PROVIDER=cloudflare
ARCHIVEBOX_PROXY_ACME_EMAIL=you@example.com
# Set =1 while testing to use Let's Encrypt staging (avoids prod rate limits):
ARCHIVEBOX_PROXY_ACME_STAGING=0
# ---- Provider credentials: pass through the vars YOUR provider needs ----
# (names come straight from https://go-acme.github.io/lego/dns/)
# cloudflare:
CLOUDFLARE_DNS_API_TOKEN=your-scoped-token
# route53 (AWS):
# AWS_ACCESS_KEY_ID=...
# AWS_SECRET_ACCESS_KEY=...
# AWS_REGION=us-east-1
# digitalocean:
# DO_AUTH_TOKEN=...
# hetzner:
# HETZNER_API_KEY=...
# gcloud:
# GCE_PROJECT=...
# GOOGLE_APPLICATION_CREDENTIALS=/path/in/container.json
# namecheap:
# NAMECHEAP_API_USER=...
# NAMECHEAP_API_KEY=...
# godaddy:
# GODADDY_API_KEY=...
# GODADDY_API_SECRET=...
# Don't forget the one-time wildcard DNS A/AAAA record at your provider:
# *.archive.example.com -> <your server's public IP>
# archive.example.com -> <your server's public IP>

65
bin/proxy_lego.sh Executable file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env sh
# DNS-01 wildcard cert sidecar for the bundled ArchiveBox reverse-proxy profile.
#
# ArchiveBox serves unlimited dynamically-generated snapshot subdomains
# (snap-<id>.<base>), so per-host cert issuance can't scale — we obtain ONE
# `*.<base>` wildcard cert via DNS-01 and let Caddy serve it for every subdomain.
#
# Provider coverage is delegated entirely to go-acme/lego, which speaks DNS-01
# to ~150 providers (cloudflare, route53/AWS, gcloud, digitalocean, hetzner,
# namecheap, godaddy, ...). We do NOT hand-code providers here: you pick one
# with ARCHIVEBOX_PROXY_DNS_PROVIDER and pass its credentials through as the
# standard env vars lego documents for that provider, e.g.
# cloudflare -> CLOUDFLARE_DNS_API_TOKEN
# route53 -> AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_REGION
# gcloud -> GCE_PROJECT / GOOGLE_APPLICATION_CREDENTIALS
# digitalocean-> DO_AUTH_TOKEN
# hetzner -> HETZNER_API_KEY
# namecheap -> NAMECHEAP_API_USER / NAMECHEAP_API_KEY
# godaddy -> GODADDY_API_KEY / GODADDY_API_SECRET
# Full list + var names: https://go-acme.github.io/lego/dns/
set -eu
DOMAIN="${ARCHIVEBOX_PROXY_DOMAIN:?set ARCHIVEBOX_PROXY_DOMAIN to your base domain, e.g. archive.example.com}"
PROVIDER="${ARCHIVEBOX_PROXY_DNS_PROVIDER:?set ARCHIVEBOX_PROXY_DNS_PROVIDER to a lego DNS provider, e.g. cloudflare}"
EMAIL="${ARCHIVEBOX_PROXY_ACME_EMAIL:?set ARCHIVEBOX_PROXY_ACME_EMAIL to a contact email for ACME}"
CERT_DIR="${ARCHIVEBOX_PROXY_CERT_DIR:-/certs}"
LEGO_PATH="${CERT_DIR}/lego"
RENEW_INTERVAL="${ARCHIVEBOX_PROXY_RENEW_INTERVAL:-43200}" # 12h
# Staging CA for testing avoids burning prod rate limits; set =1 to use it.
CA_FLAG=""
if [ "${ARCHIVEBOX_PROXY_ACME_STAGING:-0}" = "1" ]; then
CA_FLAG="--server=https://acme-staging-v02.api.letsencrypt.org/directory"
fi
mkdir -p "${LEGO_PATH}"
# lego writes wildcard certs as `_.<domain>.crt` / `.key`; publish them under
# the stable names the Caddyfile points at (ARCHIVEBOX_PROXY_TLS).
publish() {
src="${LEGO_PATH}/certificates/_.${DOMAIN}"
if [ -f "${src}.crt" ] && [ -f "${src}.key" ]; then
cp -f "${src}.crt" "${CERT_DIR}/wildcard.crt"
cp -f "${src}.key" "${CERT_DIR}/wildcard.key"
echo "[proxy_lego] published *.${DOMAIN} wildcard cert -> ${CERT_DIR}/wildcard.{crt,key}"
else
echo "[proxy_lego] expected cert at ${src}.{crt,key} not found" >&2
return 1
fi
}
# One `run` to issue, then `renew` on each later pass (lego no-ops if not due).
ACTION="run"
while true; do
echo "[proxy_lego] ${ACTION} *.${DOMAIN} via ${PROVIDER} (DNS-01)"
if lego --accept-tos --email "${EMAIL}" --dns "${PROVIDER}" \
--domains "*.${DOMAIN}" --domains "${DOMAIN}" \
--path "${LEGO_PATH}" ${CA_FLAG} "${ACTION}" ${RENEW_FLAGS:-}; then
publish || true
else
echo "[proxy_lego] lego ${ACTION} failed; will retry next interval" >&2
fi
ACTION="renew"
RENEW_FLAGS="--days 30"
sleep "${RENEW_INTERVAL}"
done

78
docker-compose.proxy.yml Normal file
View File

@ -0,0 +1,78 @@
# Opt-in bundled reverse-proxy overlay for ArchiveBox.
#
# This gives the "Plex-easy" path: ArchiveBox stays on the default
# safe-subdomains-fullreplay mode (full JS replay + per-snapshot origin
# isolation), and a bundled Caddy terminates TLS and wildcard-routes every
# dynamically-generated subdomain to it. The heavy proxy/cert deps live HERE,
# never in the core archivebox image.
#
# Usage (LOCAL, *.archivebox.localhost via Caddy's internal CA):
# docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d
# # then `caddy trust` / install Caddy's root CA to silence the browser warning,
# # or just use http://...:8000 directly for plain-HTTP local browsing.
#
# Usage (PUBLIC, real domain + automatic *.<domain> wildcard cert via DNS-01):
# cp .env.proxy.example .env # set ARCHIVEBOX_PROXY_DOMAIN + provider creds
# docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d
#
# See etc/Caddyfile and bin/proxy_lego.sh for the wildcard/cert mechanics.
services:
archivebox:
# When proxied, only Caddy is published to the host. Drop the direct
# 8000 publish so X-Forwarded-Proto can't be spoofed by a client that
# bypasses the proxy, and tell ArchiveBox to trust Caddy's forwarded
# scheme (so secure cookies + https links work end-to-end).
ports: !reset []
expose:
- "8000"
environment:
- REVERSE_PROXY_TRUST_FORWARDED_PROTO=True
# In public mode, point links/redirects at your real https URL:
# - BASE_URL=https://${ARCHIVEBOX_PROXY_DOMAIN:-archive.example.com}
caddy:
image: caddy:2-alpine
restart: unless-stopped
depends_on:
- archivebox
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
environment:
# Local default below; for public set these (see .env.proxy.example):
# ARCHIVEBOX_PROXY_SITE="*.${ARCHIVEBOX_PROXY_DOMAIN} ${ARCHIVEBOX_PROXY_DOMAIN}"
# ARCHIVEBOX_PROXY_TLS="/certs/wildcard.crt /certs/wildcard.key"
- ARCHIVEBOX_PROXY_SITE=${ARCHIVEBOX_PROXY_SITE:-*.archivebox.localhost archivebox.localhost}
- ARCHIVEBOX_PROXY_TLS=${ARCHIVEBOX_PROXY_TLS:-internal}
- ARCHIVEBOX_PROXY_ACME_EMAIL=${ARCHIVEBOX_PROXY_ACME_EMAIL:-archivebox@localhost}
volumes:
- ./etc/Caddyfile:/etc/caddy/Caddyfile:ro
- ./data/proxy/caddy:/data
- ./data/proxy/certs:/certs:ro
command: ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
# DNS-01 wildcard cert sidecar (PUBLIC mode only). Disabled by default via
# the `proxy-public` profile so local users don't need DNS provider creds.
lego:
image: goacme/lego:latest
profiles: ["proxy-public"]
restart: unless-stopped
entrypoint: ["/bin/sh", "/proxy_lego.sh"]
environment:
# proxy_lego.sh enforces that these are set (it exits with a clear
# error if any are empty); kept as soft defaults here so the local
# profile still renders without public-mode vars.
- ARCHIVEBOX_PROXY_DOMAIN=${ARCHIVEBOX_PROXY_DOMAIN:-}
- ARCHIVEBOX_PROXY_DNS_PROVIDER=${ARCHIVEBOX_PROXY_DNS_PROVIDER:-}
- ARCHIVEBOX_PROXY_ACME_EMAIL=${ARCHIVEBOX_PROXY_ACME_EMAIL:-}
- ARCHIVEBOX_PROXY_ACME_STAGING=${ARCHIVEBOX_PROXY_ACME_STAGING:-0}
# Plus your provider's credential env vars, passed straight through
# to lego (e.g. CLOUDFLARE_DNS_API_TOKEN). See bin/proxy_lego.sh.
env_file:
- path: .env
required: false
volumes:
- ./bin/proxy_lego.sh:/proxy_lego.sh:ro
- ./data/proxy/certs:/certs

View File

@ -72,18 +72,23 @@ services:
# - 127.0.0.1:8080:8080
### Example: Put Nginx in front of the ArchiveBox server for SSL termination and static file serving.
# You can also any other ingress provider for SSL like Apache, Caddy, Traefik, Cloudflare Tunnels, etc.
# Note you must set up wildcard DNS and TLS *.your.archivebox.domain because snapshots are served from unique subdomains for security.
# nginx:
# image: nginx:alpine
# ports:
# - 443:443
# - 80:80
# volumes:
# - ./etc/nginx.conf:/etc/nginx/nginx.conf
# - ./data:/var/www
### TLS / HTTPS: ArchiveBox serves the admin/web/api/public control plane and every
# archived snapshot on its own subdomain for security isolation, so a public deployment
# needs wildcard DNS + a wildcard TLS cert for *.your.archivebox.domain.
#
# The bundled, opt-in proxy overlay sets this up for you (Caddy + a go-acme/lego
# DNS-01 sidecar that fetches one *.<domain> wildcard cert covering unlimited
# snap-* subdomains). The heavy proxy/cert deps stay out of the archivebox image:
#
# # local *.archivebox.localhost (Caddy internal CA):
# docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d
#
# # public, automatic wildcard TLS (edit .env from .env.proxy.example first):
# docker compose -f docker-compose.yml -f docker-compose.proxy.yml --profile proxy-public up -d
#
# Already run your own caddy/traefik/nginx or a Cloudflare/Tailscale ingress? Just set
# BASE_URL to your https URL and REVERSE_PROXY_TRUST_FORWARDED_PROTO=True, point the
# wildcard upstream at this container, and skip the overlay.
### Example: run all your ArchiveBox traffic through a WireGuard VPN tunnel to avoid IP blocks.
# You can also use any other VPN that works at the docker/IP level, e.g. Tailscale, OpenVPN, etc.

46
etc/Caddyfile Normal file
View File

@ -0,0 +1,46 @@
# ArchiveBox bundled reverse-proxy config (opt-in, used by docker-compose.proxy.yml).
#
# Why a wildcard catch-all and not a list of hosts?
# ArchiveBox serves the admin/web/api/public control plane AND every archived
# snapshot on its OWN hostname for security isolation:
# admin.<base> web.<base> api.<base> public.<base> (control plane)
# snap-<12hex>.<base> (snapshot replay)
# <original-domain>.<base> (original-domain replay)
# These are generated dynamically from the snapshots present + the active
# SERVER_SECURITY_MODE, so there can be millions of them and they can NEVER be
# enumerated. The proxy therefore matches by SUFFIX WILDCARD only.
#
# TLS at that scale:
# On-demand / per-host cert issuance does not scale to millions of subdomains
# (and would blow ACME rate limits), so for public deployments we serve a
# SINGLE `*.<base>` wildcard cert obtained once via DNS-01 by the lego sidecar
# (see bin/proxy_lego.sh). One cert covers unlimited snap-* subdomains.
#
# Configure via env (see .env.proxy.example):
# Local (default): serves *.archivebox.localhost via Caddy's internal CA.
# Public: ARCHIVEBOX_PROXY_SITE="*.archive.example.com archive.example.com"
# ARCHIVEBOX_PROXY_TLS="/certs/wildcard.crt /certs/wildcard.key"
#
# NOTE: a single-label wildcard (`*.<base>`) does not cover multi-label
# original-domain replay hosts like `www.example.com.<base>`; those are a
# secondary feature and are reachable via their single-label `snap-<id>.<base>`
# host instead. The primary snapshot replay host IS single-label and fully covered.
{
# ACME contact email (only used if Caddy itself ever manages a cert; the
# internal CA and mounted-cert paths ignore it).
email {$ARCHIVEBOX_PROXY_ACME_EMAIL:archivebox@localhost}
}
{$ARCHIVEBOX_PROXY_SITE:*.archivebox.localhost archivebox.localhost} {
# internal -> Caddy's local CA (good for *.archivebox.localhost dev)
# /certs/c /certs/k -> the *.<base> wildcard cert from the lego sidecar
tls {$ARCHIVEBOX_PROXY_TLS:internal}
encode zstd gzip
# reverse_proxy preserves the inbound Host header and automatically sets
# X-Forwarded-Proto / -For / -Host upstream. ArchiveBox's HostRoutingMiddleware
# routes by Host, and REVERSE_PROXY_TRUST_FORWARDED_PROTO=True makes it honor
# the forwarded scheme so secure cookies + https links work end-to-end.
reverse_proxy archivebox:8000
}

View File

@ -4,6 +4,14 @@ In this folder are some example config files you can use for setting up ArchiveB
E.g. see `nginx.conf` for an example nginx config to serve your archive with SSL, or `fly.toml` for an example deployment to the Fly.io hosting platform.
For the recommended, batteries-included reverse proxy, see `Caddyfile` — it's used by
the opt-in `docker-compose.proxy.yml` overlay to wildcard-route every dynamically
generated snapshot/role subdomain to ArchiveBox and terminate TLS. Locally it serves
`*.archivebox.localhost` via Caddy's internal CA; publicly it serves a single
`*.<your-domain>` wildcard cert obtained via DNS-01 by the `goacme/lego` sidecar
(`../bin/proxy_lego.sh`), which covers unlimited `snap-*` subdomains with one cert and
supports ~150 DNS providers without any per-provider code. See `../.env.proxy.example`.
Please contribute your etc files here! Example contributions
- supervisord config