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
}
}
emailin 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 gzipcompresses the editor's JavaScript and JSON responses.reverse_proxy 127.0.0.1:5678uses the loopback address because Caddy runs on the host, not in the compose network. Caddy setsX-Forwarded-For,X-Forwarded-ProtoandX-Forwarded-Hostby default and upgrades websockets without extra lines. Those three headers are what the n8n docs ask the last proxy to forward.headeradds HSTS for one year (browsers will refuse plain HTTP to this host afterwards), blocks MIME sniffing, trims referrers and removes theServerheader.
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"
| Variable | What the n8n docs say | Default | Why we set it |
|---|---|---|---|
N8N_HOST | Host name n8n runs on | localhost | So generated links use the real domain |
N8N_PROTOCOL | The protocol used to reach n8n | http | TLS ends at Caddy, but users reach n8n over https |
WEBHOOK_URL / N8N_WEBHOOK_URL | Base URL for test and production webhooks behind a reverse proxy. WEBHOOK_URL is deprecated from 2.35.0 and logs a warning | none | Without it, n8n builds webhook URLs from its internal port 5678 |
N8N_EDITOR_BASE_URL | Public URL where users reach the editor; used for emails and SAML redirects | none | Invite and password-reset mails link to the right host |
N8N_PROXY_HOPS | Number of reverse proxies n8n runs behind | 0 | One 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. Use127.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=falsecopied 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
- n8n Docs, Configure webhook URLs with reverse proxy (2026)
- n8n Docs, Endpoints environment variables (2026)
- n8n Docs, Deployment environment variables (2026)
- Caddy Docs, Automatic HTTPS (2026)
- Caddy Docs, reverse_proxy directive (2026)
- Let's Encrypt, Challenge Types (2026)
- Cloudflare Docs, Proxy status (2026)
- Ubuntu Packages, caddy in resolute (26.04)
- MDN Web Docs, 308 Permanent Redirect (2026)



