You put n8n behind nginx, open a Webhook node, and it hands you a URL pointing at localhost:5678. You paste it into Stripe. Nothing arrives.

This is the most common self-hosted n8n problem and it is not a proxy misconfiguration. n8n is telling you the truth about the only address it knows.

Why Does n8n Show localhost as My Webhook URL?

Because of how the URL is assembled. n8n’s docs are explicit: “n8n creates the webhook URL by combining N8N_PROTOCOL, N8N_HOST and N8N_PORT. If n8n runs behind a reverse proxy, that won’t work.” N8N_HOST defaults to localhost, so that is what you get.

The proxy is doing its job. Requests arrive at https://n8n.example.com, nginx forwards them to 127.0.0.1:5678, n8n serves them. What the proxy cannot do is reach into n8n’s URL-building code and tell it which hostname the request originally used. You do that with configuration.

How Do I Change the n8n Webhook URL?

Set N8N_WEBHOOK_URL to your full public HTTPS URL and restart n8n. n8n’s docs describe it as setting “the base URL for both test and production webhooks when running n8n behind a reverse proxy.” Set N8N_EDITOR_BASE_URL to the same value so the editor’s own links match.

N8N_WEBHOOK_URL=https://n8n.example.com/
N8N_EDITOR_BASE_URL=https://n8n.example.com

A naming change worth knowing about

If you have followed an older guide, you set WEBHOOK_URL, and most n8n content on the web still says that. As of n8n 2.35.0 it is deprecated: the docs describe WEBHOOK_URL as an “alias of N8N_WEBHOOK_URL. Still works, but n8n logs a deprecation warning on startup.”

So:

Worth checking your own instance’s startup log for that warning after an update, because a deprecation warning that nobody reads is how a variable stops working two majors later.

This variable reaches further than webhooks. The MCP Server Trigger node is a webhook underneath, so it inherits the same base URL, and an instance still advertising localhost cannot be reached by a cloud MCP client at all. What the n8n MCP server actually is covers that side.

Make it survive a restart

If you run n8n from systemd, put it in a drop-in rather than editing the unit file, so a package update does not wipe it:

mkdir -p /etc/systemd/system/n8n.service.d
cat > /etc/systemd/system/n8n.service.d/webhook.conf <<'EOF'
[Service]
Environment=N8N_WEBHOOK_URL=https://n8n.example.com/
Environment=N8N_EDITOR_BASE_URL=https://n8n.example.com
EOF
systemctl daemon-reload && systemctl restart n8n

That drop-in pattern is worth internalising: n8n reads its environment from its systemd unit, so a drop-in under n8n.service.d/ is the route that survives restarts and upgrades. It is also the mechanism to use for any other n8n variable on a systemd install - the timezone, the proxy hops, all of it.

InstaPods writes this same drop-in at deploy - an n8n pod gets a n8n.service.d/webhook.conf carrying its own HTTPS domain, so webhook URLs are right on the first workflow you build rather than after your first failed integration. Ours still uses the older WEBHOOK_URL name, which is correct for the n8n version our image ships and is on the list to change.

How Do I Put n8n Behind HTTPS?

Terminate TLS at a reverse proxy and forward plain HTTP to n8n on 5678. Do not make n8n serve TLS itself. Forward the X-Forwarded-* headers - n8n’s docs require X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto from the last proxy in the path - and point N8N_WEBHOOK_URL at the public https:// address.

A minimal nginx server block:

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 100M;

    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-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Three lines in there are not optional and are the usual cause of a proxy that “works” but breaks n8n:

Then the certificate, and a renewal you remember to check:

certbot --nginx -d n8n.example.com

If you would rather not own that: on InstaPods the certificate is a wildcard, issued and renewed at the server level via certbot with DNS validation, so an n8n pod is on HTTPS at its domain from deploy with no per-pod certificate and no renewal timer of yours to discover has been broken for a month. Same TLS, different person getting paged.

What Is N8N_PROXY_HOPS For?

It tells n8n how many trusted proxies sit in front of it so it reads the right entry out of X-Forwarded-For. The default is 0. With a single nginx or Caddy in front, set it to 1.

N8N_PROXY_HOPS=1

This is not cosmetic, and n8n documents a concrete symptom for getting it wrong. From the Webhook node’s own common-issues page: “If you’re unable to connect from IP addresses in your IP whitelist, check if you are running n8n behind a reverse proxy. If so, set the N8N_PROXY_HOPS environment variable to the number of reverse-proxies n8n is running behind.”

Count actual hops. One nginx is 1. Cloudflare in front of nginx is 2.

Do I Need a Static IP for n8n Webhooks?

No. A webhook needs a public hostname that resolves to your server and a certificate the caller accepts. A dynamic IP behind a DNS record is fine. The static-IP framing sends people down the wrong path.

Work through it in order and stop at the first failure:

  1. Is the hostname public? curl https://n8n.example.com/healthz from a network that is not yours. If that fails, nothing else matters.
  2. Is the certificate valid and complete? Some API providers reject self-signed certificates and many reject an incomplete chain. Check with an external SSL checker, not your browser, which caches intermediates the caller does not have.
  3. Does N8N_WEBHOOK_URL match the hostname on the certificate? A mismatch produces a URL that resolves and then fails TLS verification at the caller.
  4. Is the workflow active? Test webhooks exist only while the editor is listening. Production webhooks require an active workflow.

Point 4 accounts for more “my webhook returns 404” reports than every configuration problem combined. Test and production webhooks are different paths, and only the production one is permanent.

The n8n Port and Bind Address

n8n listens on 5678 (N8N_PORT), and N8N_LISTEN_ADDRESS controls which interface it binds to - the default is ::, meaning everything. N8N_HOST is not the bind address; it is the hostname n8n uses to build URLs, and it defaults to localhost.

That distinction trips people up. Setting N8N_HOST=n8n.example.com changes the URLs n8n generates, not what it listens on. If you want n8n reachable only through your proxy, set N8N_LISTEN_ADDRESS=127.0.0.1 and let the proxy be the only public listener - otherwise the editor is exposed directly on 5678 over plain HTTP to anyone who scans the box.

How Do I Change the n8n Timezone?

Set GENERIC_TIMEZONE to an IANA zone like Europe/Berlin. It is what Schedule and Cron nodes use, and its default is America/New_York - not UTC, which surprises most people. Set TZ to the same value so the system clock and anything shelling out agree with it.

GENERIC_TIMEZONE=Europe/Berlin
TZ=Europe/Berlin

n8n’s docs draw the line clearly: TZ “sets the system timezone to control what scripts and commands like date return”, while GENERIC_TIMEZONE “sets the correct timezone for schedule-oriented nodes like the Schedule Trigger node.” Every official Docker example sets both to the same value. Set one and not the other and you get a workflow that fires at a time neither of you expected.

Same drop-in mechanism as above, same restart requirement.

Quick Reference

VariableWhat it doesDefaultTypical value
N8N_WEBHOOK_URLPublic base URL for test + production webhooksnonehttps://n8n.example.com/
WEBHOOK_URLDeprecated alias of the above (n8n 2.35.0+)noneuse the new name
N8N_EDITOR_BASE_URLPublic URL for editor links and n8n emailsnonehttps://n8n.example.com
N8N_HOSTHostname n8n uses when building URLslocalhostyour public host
N8N_PORTPort n8n serves on56785678
N8N_LISTEN_ADDRESSInterface n8n binds to::127.0.0.1 behind a proxy
N8N_PROTOCOLProtocol n8n believes it serveshttphttp when the proxy does TLS
N8N_PROXY_HOPSReverse proxies in front01
GENERIC_TIMEZONETimezone for Schedule/Cron nodesAmerica/New_Yorkyour zone
TZSystem timezonesystemsame as above

Skipping All of This

Everything above is a one-time setup you then own forever - the renewal, the drop-in surviving an upgrade, the proxy config surviving a distro change.

On InstaPods an n8n pod arrives with its public webhook URL and editor base URL already set to its HTTPS domain, nginx already in front with the WebSocket headers and a 100 MB body limit, and a wildcard certificate renewed on the host. It is $7/mo on the Build plan, which is the plan n8n wants anyway - see what n8n actually needs.

If you already have a VPS and a working nginx, the config above is twenty minutes and you genuinely do not need us. What you would be buying is not the twenty minutes.

FAQ

Why does my n8n webhook return 404? Either the workflow is not active - test webhooks stop existing when you close the editor - or you are using a test URL where a production URL is needed. Activate the workflow and use the production URL.

Do I need N8N_PROTOCOL=https if my proxy handles TLS? No. n8n is genuinely serving HTTP internally, so N8N_PROTOCOL stays http. N8N_WEBHOOK_URL carries the public https:// address. Setting N8N_PROTOCOL=https while n8n holds no certificate causes more problems than it solves.

Can I run n8n on a subpath like example.com/n8n? Possible, and a bad time. n8n assumes it owns the root path, and subpath deployments break editor assets and webhook paths in ways that need per-route rewriting. Use a subdomain.

My webhook worked in test and fails in production. Different URL, different lifetime. Test webhooks are registered only while the editor listens; production webhooks need the workflow active.

Where do I set these on a Docker install? As -e flags or in the compose file’s environment: block. The systemd drop-in above is for npm-installed n8n running as a service, which is what most VPS installs and InstaPods pods use.


Want the proxy, the certificate and the webhook URL handled at deploy? Deploy n8n on InstaPods for $7/mo, or compare eight n8n hosts including the DIY VPS route.