To run n8n behind Caddy with HTTPS, bind n8n to 127.0.0.1:5678, give Caddy a three-line site block with reverse_proxy 127.0.0.1:5678, point an A record at the server with any CDN proxy off, and tell n8n its public URL. Caddy then fetches a Let's Encrypt certificate by itself. Our instance at n8n.aihostinglab.com answers plain HTTP with a 308 Permanent Redirect and serves a Let's Encrypt certificate valid for 90 days.

It matters now because n8n 2.35.0 deprecated WEBHOOK_URL, the variable most tutorials still use, and because a single wrong proxy setting gives you http:// webhook links that external services reject. This page is part of our n8n hosting guide; the install itself is in install n8n on Hetzner.

Tested in the Lab Hetzner CX23 (production, Ubuntu 26.04) · Nov 2026

The setup in one picture

  • Internet → ports 80 and 443 → Caddy (systemd service from the Ubuntu repo, config in /etc/caddy/Caddyfile).
  • Caddy → 127.0.0.1:5678 → n8n in Docker. Port 5678 is not reachable from outside.
  • n8n → Postgres 16 on the internal Docker network.

We installed Caddy with apt-get -y install caddy. Ubuntu 26.04 ships caddy 2.6.2, which is older than the releases on Caddy's own repository but has everything this config uses. It runs as a normal service, so the Docker compose file stays n8n plus Postgres only.

The Caddyfile we run

{
	email you@example.com
}
n8n.aihostinglab.com {
	encode zstd gzip
	reverse_proxy 127.0.0.1:5678
	header {
		Strict-Transport-Security "max-age=31536000"
		X-Content-Type-Options nosniff
		Referrer-Policy strict-origin-when-cross-origin
		-Server
	}
}
  • email in the global block is the address the certificate authority uses for expiry notices. Use a real inbox.
  • The site address is a bare hostname. That alone switches on automatic HTTPS: certificate, renewal and the HTTP→HTTPS redirect.
  • encode zstd gzip compresses the editor's JavaScript and JSON responses.
  • reverse_proxy 127.0.0.1:5678 uses the loopback address because Caddy runs on the host, not in the compose network. Caddy sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host by default and upgrades websockets without extra lines. Those three headers are what the n8n docs ask the last proxy to forward.
  • header adds HSTS for one year (browsers will refuse plain HTTP to this host afterwards), blocks MIME sniffing, trims referrers and removes the Server header.

Apply changes and catch typos:

caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
journalctl -u caddy --no-pager | tail -20

The official n8n Hetzner guide adds flush_interval -1 to its reverse_proxy. We run without it. Caddy's docs say responses of type text/event-stream are flushed immediately regardless of that setting.

The n8n variables that matter behind a proxy

We checked each one against the n8n docs on 2026-11-05. These are the lines from our compose file:

N8N_HOST: n8n.aihostinglab.com
N8N_PROTOCOL: https
WEBHOOK_URL: https://n8n.aihostinglab.com/
N8N_EDITOR_BASE_URL: https://n8n.aihostinglab.com/
N8N_PROXY_HOPS: "1"
VariableWhat the n8n docs sayDefaultWhy we set it
N8N_HOSTHost name n8n runs onlocalhostSo generated links use the real domain
N8N_PROTOCOLThe protocol used to reach n8nhttpTLS ends at Caddy, but users reach n8n over https
WEBHOOK_URL / N8N_WEBHOOK_URLBase URL for test and production webhooks behind a reverse proxy. WEBHOOK_URL is deprecated from 2.35.0 and logs a warningnoneWithout it, n8n builds webhook URLs from its internal port 5678
N8N_EDITOR_BASE_URLPublic URL where users reach the editor; used for emails and SAML redirectsnoneInvite and password-reset mails link to the right host
N8N_PROXY_HOPSNumber of reverse proxies n8n runs behind0One proxy (Caddy), so 1. n8n then trusts the forwarded headers

We run 2.46.6 with the old WEBHOOK_URL name and publish the file as it is. On a new install, write N8N_WEBHOOK_URL instead. If you later put Cloudflare's proxy or a load balancer in front of Caddy, that is a second hop and N8N_PROXY_HOPS should match.

After any change, recreate the container so it reads the new environment:

cd /opt/n8n && docker compose up -d

The Cloudflare proxy gotcha

Our domain's DNS is on Cloudflare. Before starting Caddy, we set the A record for n8n to DNS only (grey cloud). Here is why. Caddy proves domain ownership with the ACME HTTP-01 challenge on port 80 or TLS-ALPN-01 on port 443; both are on by default. A proxied record answers DNS with Cloudflare's anycast IPs, so the validation request reaches Cloudflare first, not your server. With DNS only, the answer is your server's real IP and the challenge reaches Caddy directly. If you want the orange cloud, you need a DNS challenge instead, which means a Caddy build that includes the Cloudflare DNS module plus a Cloudflare API token. We did not need that, so we kept the stock package.

Check what the world sees:

dig +short n8n.aihostinglab.com
62.238.97.113

That is our server's address. A Cloudflare IP there means the proxy is still on.

Three checks that prove it works

1. HTTP redirects to HTTPS with 308

curl -sI http://n8n.aihostinglab.com/
HTTP/1.1 308 Permanent Redirect
Connection: close
Location: https://n8n.aihostinglab.com/
Server: Caddy

A 308 keeps the method and body, so a webhook caller that POSTs to http:// by mistake is sent to the HTTPS URL as a POST. Note the Server: Caddy line. Our -Server rule only applies inside the HTTPS site block; the redirect comes from Caddy's automatic HTTPS, outside that block. It reveals nothing beyond "this is Caddy", but if you expected the header gone everywhere, now you know.

2. HTTPS answers with our headers

curl -sI https://n8n.aihostinglab.com/
HTTP/2 200
alt-svc: h3=":443"; ma=2592000
referrer-policy: strict-origin-when-cross-origin
strict-transport-security: max-age=31536000
x-content-type-options: nosniff

(Shortened.) No server header here. The alt-svc line is Caddy offering HTTP/3, which runs over UDP port 443. Our firewall only allows 443/tcp, so browsers try HTTP/3, fail and fall back to HTTP/2. It works, but if you want HTTP/3, add ufw allow 443/udp.

3. The certificate is from Let's Encrypt

echo | openssl s_client -connect n8n.aihostinglab.com:443 -servername n8n.aihostinglab.com 2>/dev/null | openssl x509 -noout -issuer -subject -dates
issuer=C=US, O=Let's Encrypt, CN=YE2
subject=CN=n8n.aihostinglab.com
notBefore=Nov  5 21:11:31 2026 GMT
notAfter=Feb  3 21:11:30 2027 GMT

90 days, renewed by Caddy before expiry. If you see a Cloudflare issuer here, the record is proxied. Caddy's docs list ZeroSSL as the fallback issuer if Let's Encrypt fails, so a ZeroSSL issuer is also normal.

Finally, confirm n8n is not reachable around the proxy. From another machine, port 5678 on our server does not answer, and locally the health check still works:

curl -s http://127.0.0.1:5678/healthz
{"status":"ok"}

Common mistakes

  • ports: "5678:5678" in compose. Docker's published ports skip ufw, so n8n is public over plain HTTP. Use 127.0.0.1:5678:5678.
  • Port 80 closed. HTTP-01 only works on port 80. Our firewall allows 80 and 443.
  • Serving n8n under a sub-path like /n8n/. Use a subdomain; it needs no extra path settings.
  • N8N_SECURE_COOKIE=false copied from a test setup. Our load-test script sets it because it talks plain HTTP on localhost. Production never needs it.

Once HTTPS is live, n8n shows its owner-setup page to anyone who opens the URL. Create the owner account straight away.

Next step: make sure the server under Caddy is locked down with our Ubuntu VPS hardening script, explained line by line.

FAQ

Which n8n environment variables matter behind Caddy?

N8N_HOST, N8N_PROTOCOL=https, the public webhook URL (N8N_WEBHOOK_URL on n8n 2.35.0 and later, WEBHOOK_URL before), N8N_EDITOR_BASE_URL and N8N_PROXY_HOPS=1 for a single proxy.

Does Caddy need extra config for n8n websockets?

No. Caddy's reverse_proxy handles the websocket upgrade by default and sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host, which the n8n docs ask the last proxy to forward.

Can I keep the Cloudflare orange cloud on?

Not with this simple setup. A proxied record answers DNS with Cloudflare IPs, so the certificate challenge and all traffic hit Cloudflare first. Set the record to DNS only, or switch Caddy to a DNS challenge, which needs a Caddy build with the Cloudflare DNS module.

How do I check that the certificate is from Let's Encrypt?

Run openssl s_client against your domain and pipe it to openssl x509 -noout -issuer. Our server returns an issuer with O=Let's Encrypt.

Why does n8n generate http:// webhook URLs?

n8n builds URLs from its own host, protocol and port unless you set the public webhook URL. Behind a proxy it only sees port 5678, so set the HTTPS webhook URL explicitly.

Sources

  1. n8n Docs, Configure webhook URLs with reverse proxy (2026)
  2. n8n Docs, Endpoints environment variables (2026)
  3. n8n Docs, Deployment environment variables (2026)
  4. Caddy Docs, Automatic HTTPS (2026)
  5. Caddy Docs, reverse_proxy directive (2026)
  6. Let's Encrypt, Challenge Types (2026)
  7. Cloudflare Docs, Proxy status (2026)
  8. Ubuntu Packages, caddy in resolute (26.04)
  9. MDN Web Docs, 308 Permanent Redirect (2026)