n8n Webhooks Not Working? Set WEBHOOK_URL, HTTPS and the Reverse Proxy Correctly

A self-hosted n8n instance often works fine in the editor, and then the first real webhook fails. The URL shows http://localhost:5678, an external service refuses to register it, or the editor keeps saying "Connection lost". These problems almost always come from the same three places: the webhook URL n8n thinks it has, HTTPS, and the reverse proxy in front of n8n. This guide goes through each one, with the configuration that works and the error messages that point to each mistake.
Test URL vs production URL
Every Webhook node has two URLs, and confusing them is the most common cause of a 404.
Test URL (
/webhook-test/your-path): works only while the editor is listening, after you click the button to listen for a test event. It accepts one call, and the data appears on the canvas.Production URL (
/webhook/your-path): works only when the workflow is active. Calls do not appear on the canvas. You see them in the Executions list.
If you call a URL that is not listening, n8n answers with a 404 and a message like The requested webhook "POST your-path" is not registered., usually with a hint to activate the workflow or to start listening in the editor. If the path exists but the HTTP method is wrong, you get This webhook is not registered for GET requests. Did you mean to make a POST request?. Check the method in the Webhook node first.
Test from outside the server with curl, so you see exactly what an external service sees:
curl -i -X POST https://n8n.example.com/webhook/your-path \
-H "Content-Type: application/json" \
-d '{"ping": true}'
Set WEBHOOK_URL
n8n builds the webhook URLs it shows in the editor and sends to external services from its own settings. Without help, it uses N8N_PROTOCOL, N8N_HOST and N8N_PORT, which behind a proxy gives you something like http://localhost:5678/webhook/.... That URL is useless to anyone outside the server.
Set the public address explicitly:
N8N_HOST=n8n.example.com
N8N_PROTOCOL=https
N8N_PORT=5678
WEBHOOK_URL=https://n8n.example.com/
N8N_PROXY_HOPS=1
WEBHOOK_URL is the important one. It must be the public HTTPS address, with the trailing slash, and without /webhook at the end; n8n adds the path itself. If your n8n runs under a subpath, include it, for example https://example.com/n8n/, and also set N8N_PATH=/n8n/.
In Docker Compose, put these under the n8n service's environment and recreate the container with docker compose up -d. A plain restart does not pick up changes in the compose file.
One more step is easy to miss. Trigger nodes for services such as Telegram, Stripe or GitHub register their webhook with the service when you activate the workflow. After you change WEBHOOK_URL, deactivate and activate those workflows again so the service receives the new address.
Why N8N_PROXY_HOPS matters
Behind a reverse proxy, every request reaches n8n from the proxy's address, and the real client IP travels in the X-Forwarded-For header. n8n trusts that header only if you tell it how many proxies sit in front of it. Without this setting, the logs fill with:
ValidationError: The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default).
Set N8N_PROXY_HOPS to the number of proxies in the path. One Nginx or Caddy on the same server is 1. If you also run a CDN or a load balancer in front of it, count that too. This fixes client IPs in logs and keeps rate limiting working on real users, not on your proxy.
HTTPS is not optional
Most services that send webhooks only accept HTTPS endpoints with a valid certificate. n8n also sets its login cookie as secure by default, so if you open the editor over plain HTTP you see:
Your n8n server is configured to use a secure cookie, however you are either visiting this via an insecure URL, or using Safari.
The fix is to put HTTPS in front of n8n, not to disable secure cookies. N8N_SECURE_COOKIE=false is acceptable only for a short local test.
A working Nginx configuration
Keep n8n listening on 127.0.0.1:5678 and let Nginx handle TLS. The editor uses a WebSocket connection for live updates, so the proxy must pass the Upgrade and Connection headers. Without them you get the "Connection lost" banner and executions do not update in the editor, even though webhooks may still work.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name n8n.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name n8n.example.com;
ssl_certificate /etc/letsencrypt/live/n8n.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/n8n.example.com/privkey.pem;
client_max_body_size 16m;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 300s;
}
}
Get the certificate with Certbot, then test and reload Nginx:
sudo certbot certonly --nginx -d n8n.example.com
sudo nginx -t && sudo systemctl reload nginx
If you issue the certificate before the HTTPS server block exists, comment out the 443 block first, or let Certbot edit the configuration for you with sudo certbot --nginx.
Three lines in this file solve specific errors:
client_max_body_size: Nginx allows 1 MB by default. Larger webhook payloads fail with413 Request Entity Too Largebefore they reach n8n. 16 MB matches n8n's default payload limit (N8N_PAYLOAD_SIZE_MAX).proxy_read_timeout: if a webhook waits for the workflow to finish before it responds, a long workflow can hit the default 60-second timeout and return504 Gateway Timeout.X-Forwarded-Proto: tells n8n the original request was HTTPS.
The Caddy alternative
Caddy gets and renews certificates automatically and passes WebSocket and forwarding headers by default. The whole configuration is:
n8n.example.com {
reverse_proxy 127.0.0.1:5678
}
Long-running webhooks
If an external service calls your webhook and waits for the answer, keep the response fast. Many senders give up after a few seconds and retry, which can run your workflow twice. Set the Webhook node to respond immediately, or add a Respond to Webhook node early in the workflow and do the heavy work after it. If a CDN sits in front of your server, remember that it has its own timeout that your Nginx settings cannot change.
Quick checklist
The workflow is active and you call the
/webhook/URL, not/webhook-test/.The HTTP method in the Webhook node matches the sender.
WEBHOOK_URLis the public HTTPS address with a trailing slash, and the container was recreated.Trigger workflows for external services were deactivated and activated again after the URL change.
DNS points to the server, and ports 80 and 443 are open in the firewall.
The proxy passes
Upgrade,ConnectionandX-Forwarded-*headers, andN8N_PROXY_HOPSmatches the number of proxies.curl -ifrom outside returns a 200, not a 404, 413 or 504.
Run it on IM HOST
IM HOST n8n Hosting comes with n8n pre-installed with HTTPS and PostgreSQL, so the proxy and certificate are in place from the start. You get unlimited workflows and executions and full root access if you want to adjust the configuration above. If you prefer to set up everything yourself, a Linux VPS gives you full root on KVM with a dedicated IPv4 address, from $5/month. Our engineers are available 24/7 if a webhook still refuses to fire.
More from our blog
Discover more practical guides and product insights from the IM Host team.
View all articles