infra(compose): add Caddy edge reverse proxy with ACME (issue #155) - #160
Merged
Conversation
HTTPS edge transport per ADR-025: Caddy terminates TLS with automatic ACME issuance/rotation (HTTP-01) and is the sole public entry point. The api container is now Docker-internal only (host port :3000 removed). - docker-compose.prod.yml: caddy service (digest-pinned 2.9.1-alpine), :80/:443 only, caddy_data/caddy_config volumes (LUKS volume, ADR-023), DOMAIN required; api ports removed - Caddyfile (prod): domain placeholder, TLS 1.2+ floor, HSTS + nosniff + -Server, request_body ceiling aligned with PHOTO_MAX_SIZE_MB, default-deny posture (unknown host :80 -> 404, unknown SNI -> TLS reject, unknown paths -> app 404), DNS-01 documented as follow-up (ADR-027) - docker-compose.caddy.yml + Caddyfile.dev: local HTTPS dev overlay (tls internal, host networking) - runbooks.md: Caddy tier, boot prerequisites, ACME-state backup, digest-pinning, HTTPS edge section - ci.yml: compose-config job validating all compose combinations + caddy validate for both Caddyfiles - docs/issues/issue-155: plan + verified results Verified: docker compose config (4 combos), caddy validate, local HTTPS smoke test (HSTS, 308 redirect, 404 default-deny, 413 body limit), gitleaks clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements ADR-025 (issue #155): HTTPS edge transport via Caddy as the sole public entry point, with automatic ACME certificate issuance/rotation (HTTP-01 by default). The
apicontainer becomes Docker-internal only — its host port publication (:3000) is removed from the production compose.Changes
backend/docker-compose.prod.ymlcaddyservice, digest-pinned (caddy:2.9.1-alpine@sha256:b4e39523…a534f0)::80(HTTP→HTTPS redirect + ACME HTTP-01 challenge) and:443(TLS). Admin API (:2019) stays internal-only.DOMAINis now a required env var (${DOMAIN:?DOMAIN is required});PHOTO_MAX_SIZE_MBis passed to Caddy so the edge ceiling stays aligned with the API../Caddyfileread-only; persistentcaddy_data(ACME account + certs) andcaddy_configvolumes — to be placed on the LUKS-encrypted volume (ADR-023).apiservice: hostports:removed — reachable only on the Docker network from Caddy.backend/Caddyfile(new, version-controlled){$DOMAIN}; explicit modern-TLS floor (tls1.2 tls1.3).max-age=31536000; includeSubDomains),X-Content-Type-Options: nosniff,Serverheader removed.request_body { max_size {$PHOTO_MAX_SIZE_MB:20}MB }aligned with the API.:80→ explicit404catch-all; unknown SNI on:443→ TLS-layer reject (no cert served); unknown paths → the API's own 404 fallback. Nofile_server, no catch-all that returns 200. (The:80catch-all was verified not to interfere with automatic HTTPS / the HTTP-01 challenge; a:443catch-all is deliberately avoided to prevent ACME rate-limit abuse.)Dev overlay (optional scope item)
backend/Caddyfile.dev+backend/docker-compose.caddy.yml: local HTTPS testing with Caddy's internal CA (tls internal),network_mode: host→ proxies to127.0.0.1:3000(host-runcargo run -p api). Plain tag allowed for dev, per the issue.Docs
backend/docs/operations/runbooks.md: Caddy tier, boot prerequisites (DOMAIN),caddy_databackup (ACME state / rate-limit protection), digest-pinning entry, healthcheck note, new § HTTPS edge & certificates (ADR-025) incl. DNS-01 follow-up (vault-sourced per ADR-027).backend/docs/issues/issue-155-caddy-edge-reverse-proxy.md: systematic implementation plan + verified results.CI guardrail
.github/workflows/ci.yml— newcompose-configjob:docker compose config --quietfor all four compose combinations andcaddy validatefor both Caddyfiles (daemon-free; digest-pinned image), mechanically enforcing the “docker compose configvalidates” acceptance criterion.Verification
docker compose configpasses for prod, dev+caddy, dev+idp, dev+idp+caddycaddy validatepasses forCaddyfileandCaddyfile.dev(env placeholders resolved)Strict-Transport-Security: max-age=31536000; includeSubDomains,nosniff, noServerheader308redirect:80→404; unknown SNI on:443→ TLS-level reject; unknown path → proxied app404413; 5 MB body →200(edge ceiling aligned withPHOTO_MAX_SIZE_MB)gitleaks detect— no leaksOut of scope (tracked elsewhere, per issue)
/swagger-uiproduction authz lockdown — deployment concern (ADR-025):80/:443— ADR-026 / VPS runbookOpen questions (before first prod boot)
DOMAINvalue and DNS provider (HTTP-01 default).Closes #155
Co-authored-by: deepseek-v4-flash (opencode-go)