From a7514a7eb805cc774e032efa14c505880a39c107 Mon Sep 17 00:00:00 2001 From: archivebox Date: Thu, 4 Jun 2026 22:15:35 +0000 Subject: [PATCH] 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 *. 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. --- .env.proxy.example | 42 ++++++++++++++++++++++ bin/proxy_lego.sh | 65 +++++++++++++++++++++++++++++++++ docker-compose.proxy.yml | 78 ++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 29 ++++++++------- etc/Caddyfile | 46 ++++++++++++++++++++++++ etc/README.md | 8 +++++ 6 files changed, 256 insertions(+), 12 deletions(-) create mode 100644 .env.proxy.example create mode 100755 bin/proxy_lego.sh create mode 100644 docker-compose.proxy.yml create mode 100644 etc/Caddyfile diff --git a/.env.proxy.example b/.env.proxy.example new file mode 100644 index 00000000..77bc1317 --- /dev/null +++ b/.env.proxy.example @@ -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-*.) ---- +ARCHIVEBOX_PROXY_DOMAIN=archive.example.com + +# Tell Caddy to serve the single *. 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 -> +# archive.example.com -> diff --git a/bin/proxy_lego.sh b/bin/proxy_lego.sh new file mode 100755 index 00000000..f9372350 --- /dev/null +++ b/bin/proxy_lego.sh @@ -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-.), so per-host cert issuance can't scale — we obtain ONE +# `*.` 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 `_..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 diff --git a/docker-compose.proxy.yml b/docker-compose.proxy.yml new file mode 100644 index 00000000..f92e9e33 --- /dev/null +++ b/docker-compose.proxy.yml @@ -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 *. 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 diff --git a/docker-compose.yml b/docker-compose.yml index d8374217..c452337a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 *. 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. diff --git a/etc/Caddyfile b/etc/Caddyfile new file mode 100644 index 00000000..227b2039 --- /dev/null +++ b/etc/Caddyfile @@ -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. web. api. public. (control plane) +# snap-<12hex>. (snapshot replay) +# . (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 `*.` 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 (`*.`) does not cover multi-label +# original-domain replay hosts like `www.example.com.`; those are a +# secondary feature and are reachable via their single-label `snap-.` +# 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 *. 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 +} diff --git a/etc/README.md b/etc/README.md index 1b7f0865..c385e251 100644 --- a/etc/README.md +++ b/etc/README.md @@ -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 +`*.` 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