Skip to content
Guides & Tutorials

How to Fix n8n Webhooks Behind Nginx

In this article13 sections

When self-hosted n8n sits behind Nginx and something does not work, the cause is almost always one of six settings: WEBHOOK_URL is missing, so n8n shows webhook addresses with localhost:5678; Nginx is not passing the WebSocket upgrade, so the editor says “Connection lost”; the proxy timeout is shorter than your slowest workflow; the upload limit is still 1 MB; n8n does not know it is behind a proxy; or the encryption key was never saved, so credentials cannot be read after a reinstall. Fix those six and n8n behaves the same as the hosted version.

This guide assumes n8n is already installed on an Ubuntu VPS, with npm or Docker, and that you have a domain such as n8n.example.com pointing at the server. Replace that name with your own everywhere below. If you have not checked the server itself yet, do the 15-minute VPS audit first.

How the pieces fit

n8n listens on port 5678 and speaks plain HTTP. Nginx listens on 443, handles the SSL certificate, and passes each request to n8n on the same machine. n8n therefore never sees the public address or the fact that the visitor used HTTPS unless you tell it. Half the problems below come from that one gap.

Setting 1: WEBHOOK_URL

Symptom: the Webhook node shows a URL like http://localhost:5678/webhook/..., and services such as Stripe, Telegram or GitHub cannot reach it. OAuth logins fail with a redirect URL mismatch for the same reason.

Fix: tell n8n its public address. These are environment variables, set in your .env file, docker-compose.yml or systemd unit:

N8N_HOST=n8n.example.com
N8N_PROTOCOL=https
N8N_PORT=5678
WEBHOOK_URL=https://n8n.example.com/
N8N_EDITOR_BASE_URL=https://n8n.example.com/

Restart n8n afterwards. Workflows that registered a webhook with an outside service before the change must be deactivated and activated again, so that they register the new address.

Setting 2: WebSocket upgrade headers

Symptom: the editor opens, but a red “Connection lost” notice appears, and manual test runs never show their result.

Fix: the editor keeps a live connection open to the server, and Nginx drops it unless you pass the upgrade headers. This is a complete server block for the HTTPS side:

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 50m;

    location / {
        proxy_pass http://127.0.0.1:5678;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header 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_read_timeout 300s;
        proxy_send_timeout 300s;
        proxy_buffering off;
    }
}

Test and reload with sudo nginx -t && sudo systemctl reload nginx. If you use Cloudflare in front of the server, check that WebSockets are switched on for the domain there as well.

Setting 3: Proxy timeouts

Symptom: a webhook that waits for the workflow to finish returns 504 Gateway Time-out after exactly 60 seconds, although the execution log shows the workflow completed.

Fix: Nginx gives up after 60 seconds by default. The proxy_read_timeout 300s line above raises that to five minutes. A better fix for slow workflows is to set the Webhook node to respond Immediately and do the work afterwards, because the service calling you has its own timeout that you cannot change.

Setting 4: Upload size

Symptom: posting a file to a webhook, or uploading in a form trigger, returns 413 Request Entity Too Large.

Fix: two limits apply, and both must allow the file. Nginx defaults to 1 MB, which client_max_body_size 50m raises. n8n has its own limit of 16 MB, raised with:

N8N_PAYLOAD_SIZE_MAX=50

The value is in megabytes. Large files held in memory are the usual reason an n8n server runs out of RAM, so raise the limit only as far as you need.

Setting 5: Trust the proxy, and listen only locally

Symptom: every execution appears to come from 127.0.0.1, or the log warns about an unexpected X-Forwarded-For header. Separately, n8n may be reachable on http://your-server-ip:5678, bypassing Nginx and SSL entirely.

Fix: tell n8n that exactly one proxy stands in front of it, and stop it listening on the public address:

N8N_PROXY_HOPS=1
N8N_LISTEN_ADDRESS=127.0.0.1

With Docker, the listen address is set where you publish the port instead: use 127.0.0.1:5678:5678, not 5678:5678. Confirm from another machine that http://your-server-ip:5678 no longer answers.

Warning. Docker’s published ports are not blocked by UFW. A container started with -p 5678:5678 is open to the internet even if your firewall shows that port as closed.

Setting 6: The encryption key

Symptom: after a reinstall, a move to another server or a rebuilt container, every credential shows an error and has to be typed in again.

Fix: n8n encrypts stored credentials with a key that it generates on first start and saves in its config file. If the key changes, the saved credentials cannot be decrypted, and there is no way to recover them. Set the key yourself and keep a copy somewhere safe:

N8N_ENCRYPTION_KEY=paste-a-long-random-string-here

Generate one with openssl rand -hex 32. Do this before you create your first credential. On an existing install, copy the current key out of the config file in n8n’s data folder (~/.n8n, or the mounted volume in Docker) and set that exact value.

Keep the database from filling the disk

n8n saves the full input and output of every execution. On a busy server that is the fastest-growing data on the disk. Switch pruning on and keep a week:

EXECUTIONS_DATA_PRUNE=true
EXECUTIONS_DATA_MAX_AGE=168
EXECUTIONS_DATA_PRUNE_MAX_COUNT=10000

The age is in hours. If you use PostgreSQL, which is the right choice for anything beyond a personal setup, add a nightly database dump: your workflows and credentials live in that database, not in files.

Test it end to end

  1. Create a workflow with a Webhook node set to POST and a Respond to Webhook node. The URL shown must start with https://n8n.example.com/.
  2. Click Listen for test event and call the Test URL:
    curl -i -X POST https://n8n.example.com/webhook-test/your-path -H 'Content-Type: application/json' -d '{"ping":true}'

    The editor should show the data arriving without a page reload. That proves both the webhook address and the live connection.

  3. Activate the workflow and call the Production URL (/webhook/ instead of /webhook-test/). The test URL works only while the editor is listening; the production URL works only while the workflow is active. Mixing them up is the most common “my webhook returns 404” report.

Which symptom points to which setting

You see Setting to check
Webhook URL contains localhost or http:// 1: WEBHOOK_URL
“Connection lost” in the editor 2: upgrade headers
504 after 60 seconds 3: proxy_read_timeout
413 on uploads 4: both size limits
n8n answers on port 5678 directly 5: listen address
Credentials broken after a move 6: encryption key
Disk filling up Execution pruning

Common mistakes to avoid

  • Leaving port 5678 public “for now”. The login page then travels without encryption, and so does everything you type into it.
  • Changing WEBHOOK_URL without re-activating workflows. Outside services keep calling the old address.
  • Backing up the files but not the database. With PostgreSQL, the workflows are in the database.
  • Losing the encryption key. A database backup without the key restores workflows with unusable credentials.
  • Raising every timeout to an hour. Answer the caller immediately instead and process in the background.

Running n8n on your own server

A SiteHarbour VPS can be delivered with n8n already installed: PostgreSQL as the database, Nginx as the reverse proxy on your domain, and a free SSL certificate. Self-hosting gives you unlimited workflows and executions and keeps your data on your own server. VPS-1 (2 vCores, 4 GB RAM) suits most single-user setups; choose VPS-2 for heavy workflows.

Webhooks need a stable public HTTPS address, so the certificate matters as much as the proxy. How to set up free SSL on Nginx with Certbot covers the certificate, and how to host multiple websites on one VPS with Nginx shows how to run n8n beside other sites on the same server.

Frequently asked questions

Why does my webhook work in the editor but return 404 from the outside?

You are calling the production URL of a workflow that is not active, or the test URL while the editor is not listening. Activate the workflow and use the URL without -test.

Do I need a separate domain for webhooks?

No. One domain for the editor and webhooks is fine for a single server. A separate domain only matters when you split n8n into several processes.

How much RAM does n8n need?

n8n with PostgreSQL and Nginx runs comfortably in 2 GB for light use. Memory use grows with the size of the data passing through workflows, not with the number of workflows, so large files and big API responses are what push it up.

SQLite or PostgreSQL?

SQLite is fine for trying n8n out. Use PostgreSQL for anything you depend on: it copes with concurrent executions and is straightforward to back up and restore.

Can I run n8n next to a website on the same VPS?

Yes. Give n8n its own subdomain and server block, as above, and let the website keep its own. Watch memory when both are busy at once.

The short version: n8n behind Nginx needs to be told its public address, Nginx needs to pass the live connection and allow enough time and size, n8n should listen only on the local address, and the encryption key must be saved before you store your first credential.

Share this guide