n8n behind Caddy
Config + gotchas checked against the official n8n docs and the issues people actually hit · 2026-06-07
A Caddy reverse-proxy setup for n8n, with automatic HTTPS on n8n.example.com. Copy it, swap the domain — the gotchas that trip people up are solved below.
New to reverse proxies? Start with the Caddy + Docker guide.
# Caddyfile for n8n
# Replace n8n.example.com with your real domain. Caddy issues TLS automatically.
n8n.example.com {
encode zstd gzip
reverse_proxy n8n:5678
}
Replace n8n.example.com with your domain. Your DNS A record must already point at the server.
Gotchas
What trips people up
Webhook URLs in the editor (and the ones registered with external services) show http:// or the container host, not your domain.
n8n derives webhook URLs from env, not the request. Set `WEBHOOK_URL=https://n8n.example.com/`, `N8N_HOST=n8n.example.com`, and `N8N_PROTOCOL=https` in `.env`. With one proxy in front, also set `N8N_PROXY_HOPS=1` so n8n trusts X-Forwarded-Proto.
OAuth credential redirects fail, or n8n warns about insecure cookies behind the proxy.
Same root cause — n8n thinks it is on http. `N8N_PROTOCOL=https` + `N8N_PROXY_HOPS=1` make it honor the X-Forwarded-Proto Caddy already sends, so OAuth callbacks and secure cookies use https.
Deploy
How to deploy
- Create the shared proxy network once: `docker network create proxy`.
- Set `WEBHOOK_URL`, `N8N_HOST`, `N8N_PROTOCOL=https`, `N8N_PROXY_HOPS=1` in `.env`.
- Start the stack: `docker compose up -d`, then point n8n.example.com at the server (ports 80/443 reachable for the cert).
Runnable
Full runnable stack
n8n + Caddy + the shared network, in one compose file — so it runs on the first try, not after you stitch the proxy in yourself. Save the Caddyfile above next to it.
# docker-compose.yml — n8n + Caddy, ready to run.
# Pin caddy to a digest for production: caddy:2.8-alpine@sha256:<digest>
# 1. Save the Caddyfile above as ./Caddyfile
# 2. docker network create proxy
# 3. docker compose up -d
services:
n8n:
image: docker.n8n.io/n8nio/n8n:1.68.1@sha256:5e8acf8527ddf9d08a4f0e7ac042eb3fb31a6afa071ed2b8be2e47327b0acd49
restart: unless-stopped
depends_on:
- n8n-postgres
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: n8n-postgres
DB_POSTGRESDB_PORT: "5432"
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: ${DB_PASSWORD}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
N8N_HOST: ${N8N_HOST}
N8N_PORT: "5678"
N8N_PROTOCOL: https
WEBHOOK_URL: https://${N8N_HOST}/
GENERIC_TIMEZONE: ${TZ}
N8N_RUNNERS_ENABLED: "true"
N8N_BLOCK_ENV_ACCESS_IN_NODE: "true"
volumes:
- n8n-data:/home/node/.n8n
networks:
- default
- proxy
n8n-postgres:
image: postgres:16-alpine@sha256:16bc17c64a573ef34162af9298258d1aec548232985b33ed7b1eac33ba35c229
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: n8n
POSTGRES_INITDB_ARGS: --data-checksums
volumes:
- n8n-postgres-data:/var/lib/postgresql/data
healthcheck:
test:
- CMD-SHELL
- pg_isready -U n8n -d n8n
interval: 10s
timeout: 5s
retries: 5
caddy:
image: caddy:2.8-alpine
restart: unless-stopped
ports:
- 80:80
- 443:443
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
networks:
- proxy
volumes:
n8n-data: {}
n8n-postgres-data: {}
caddy-data: {}
caddy-config: {}
networks:
proxy:
external: true