Skip to main content

Caddy Reverse Proxy & TLS

A single shared Caddy instance fronts everything, routing each domain to the right container and handling TLS automatically. It lives outside either environment (its Caddyfile is at /app/Caddyfile) and joins both Docker networks so it can reach staging and production containers alike.

Routing by domain and container name

Caddy routes by domain → container name, using the full {project}-{service}-{number} names that the -p project flag produces (staging-api-1, production-frontend-1, and so on). A minimal Caddyfile routes each app domain's /auth/* and /api/* to that environment's API and everything else to its frontend, and each n8n domain to its n8n container:

staging.circus.sh {
encode gzip
reverse_proxy /auth/* staging-api-1:4000
reverse_proxy /api/* staging-api-1:4000
reverse_proxy staging-frontend-1:3000
}
n8n-staging.circus.sh {
reverse_proxy staging-n8n-1:5678
}

(Production is the same with its own container names.)

Automatic TLS

Caddy provisions and renews Let's Encrypt certificates automatically — no manual SSL config. Two consequences: ports 80 and 443 must be open before the first deploy (the firewall step allows them), and the first request to each domain is slow while Caddy fetches the certificate; subsequent requests are fast.

Don't forget gzip

Include encode gzip on the app domains — without it, the frontend JS bundles transfer uncompressed.

How it's deployed

The shared Caddy compose file (deploy/docker-compose.caddy.yml) is copied to /app/deploy/ by CI, and the deploy scripts start Caddy on the first deploy and restart it on subsequent ones — so once the Caddyfile is in place, the pipeline manages Caddy for you.

A related gotcha: the API sits behind Caddy, so it must set trust proxy for correct client-IP detection and secure cookies. That's an app-config detail, but worth knowing when debugging cookie or rate-limit behaviour.