n8n behind Caddy

Verified · Caddy 2.8

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
# 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

  1. Create the shared proxy network once: `docker network create proxy`.
  2. Set `WEBHOOK_URL`, `N8N_HOST`, `N8N_PROTOCOL=https`, `N8N_PROXY_HOPS=1` in `.env`.
  3. 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 (app + Caddy)
# 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

Full n8n stack (compose + env + docs)