Running n8n Behind an Nginx Reverse Proxy with HTTPS: The Complete Guide
Published 2 August 2026 · 7 min read
You have installed n8n with Docker on a VPS: the instance runs, but it answers over HTTP on port 5678. To expose it properly at https://n8n.yourdomain.com, you need a reverse proxy — and if your server already hosts other sites, that reverse proxy is probably Nginx. This guide provides a complete, working configuration: the server block with the WebSocket support the editor depends on, a Let's Encrypt certificate via certbot, the n8n environment variables to adjust, and how to verify webhooks. We have already covered Traefik and Caddy for serving n8n over HTTPS; this article focuses on Nginx, the most common option on existing servers.
Why put a reverse proxy in front of n8n
Exposing port 5678 directly to the internet creates three problems. First, security: without TLS, your login credentials and workflow data travel in plain text. Second, OAuth integrations: Google, Slack, and Notion reject http:// callback URLs, so some of your credentials can never be set up. Third, shared hosting: a VPS rarely serves a single application, and Nginx lets you route n8n.yourdomain.com to n8n and www.yourdomain.com to your website, all behind the same ports 80 and 443.
The reverse proxy terminates HTTPS on the public side and forwards requests to n8n over local HTTP. Centralized TLS termination also simplifies certificate management: one tool (certbot) renews certificates for all your applications.
Prerequisites
Before touching Nginx, check these three points:
- A VPS with Nginx installed and ports 80/443 open — if you are starting from scratch, our guide to choosing a VPS for self-hosted n8n covers the criteria.
- A subdomain pointing to the server's public IP through a DNS A record (for example
n8n.yourdomain.com). - n8n running in Docker on port 5678, bound to localhost only. If that is not done yet, follow the guide to installing n8n with Docker, with one important detail in the
docker-compose.yml:
ports:
- "127.0.0.1:5678:5678"
By binding the port to 127.0.0.1, n8n is only reachable from the server itself: Nginx becomes the single entry point, and nobody can bypass HTTPS by hitting http://your-ip:5678 directly.
The Let's Encrypt certificate with certbot
Start with a minimal block so certbot can validate the domain. In /etc/nginx/sites-available/n8n.conf:
server {
listen 80;
server_name n8n.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:5678;
}
}
Enable it, then request the certificate:
sudo ln -s /etc/nginx/sites-available/n8n.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d n8n.yourdomain.com
The --nginx plugin obtains the certificate, adds the ssl_certificate directives to the file, and sets up the HTTP-to-HTTPS redirect. Renewal is automatic through a systemd timer; validate it with sudo certbot renew --dry-run.
This automation is not a luxury. A study by Krombholz, Mayer, Schmiedecker, and Weippl presented at USENIX Security in 2017, "I Have No Idea What I'm Doing" – On the Usability of Deploying HTTPS, asked experienced participants to configure TLS on a web server by hand: most produced vulnerable or incomplete configurations, even though they considered themselves competent. And a study by Aertsen, Korczyński, Moura, and their co-authors published in 2017 at the Applied Networking Research Workshop, No domain left behind: is Let's Encrypt democratizing encryption?, shows that Let's Encrypt succeeded precisely at covering the low-budget hosting segment — the typical profile of an n8n VPS. In other words: let certbot handle TLS, and focus your attention on the proxy block below, where mistakes actually happen.
The complete Nginx configuration (with WebSocket)
Once the certificate is in place, replace the contents of the HTTPS block certbot generated with this full version:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl http2;
server_name n8n.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/n8n.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/n8n.yourdomain.com/privkey.pem;
# Large uploads (workflow imports, binary files)
client_max_body_size 50m;
location / {
proxy_pass http://127.0.0.1:5678;
# WebSocket: required by the n8n editor
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Headers forwarded to n8n
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;
# Long-running workflows: do not cut the connection
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_buffering off;
}
}
server {
listen 80;
server_name n8n.yourdomain.com;
return 301 https://$host$request_uri;
}
The map block at the top of the file (outside the server block, at the http level) and the three WebSocket directives are the part everyone forgets — and the number one cause of the "Connection lost" symptom in the editor. n8n uses a persistent WebSocket connection for the editing UI and the real-time execution view; without Upgrade/Connection, Nginx downgrades the connection to a plain HTTP request and the editor keeps dropping.
The remaining directives each deserve a one-line explanation:
X-Forwarded-Proto $schemetells n8n the original request was HTTPS, which prevents URLs being generated withhttp://and secure-cookie issues.X-Forwarded-Forpasses along the client's real IP — useful for logs and for securing your webhooks with IP filtering.proxy_read_timeout 300skeeps Nginx from cutting the connection after 60 seconds (its default) on a long webhook-triggered workflow. Adjust to match your use cases.client_max_body_size 50mraises the upload limit (1 MB by default in Nginx), needed as soon as your workflows receive files.
Test and reload: sudo nginx -t && sudo systemctl reload nginx.
The n8n environment variables to adjust
Nginx only does half the job: n8n needs to know which public URL it is served under, otherwise it keeps generating links to http://localhost:5678. In the docker-compose.yml:
environment:
- N8N_HOST=n8n.yourdomain.com
- N8N_PROTOCOL=https
- N8N_PORT=5678
- WEBHOOK_URL=https://n8n.yourdomain.com/
- N8N_EDITOR_BASE_URL=https://n8n.yourdomain.com/
- N8N_PROXY_HOPS=1
N8N_HOSTandN8N_PROTOCOLdefine the instance's public identity.WEBHOOK_URLis the most important variable: it determines the URL displayed in Webhook nodes and registered with third-party services. Without it, your webhooks point to an unreachable address.N8N_EDITOR_BASE_URLis used for links generated toward the editor (invitation emails, OAuth callbacks).N8N_PROXY_HOPS=1tells n8n it sits behind one trusted proxy, so it interprets theX-Forwarded-*headers correctly.
Details on each of these (and the rest) are in our n8n environment variables guide. Then restart the container: docker compose up -d.
Testing webhooks behind the proxy
Create a minimal workflow with a Webhook node, activate it, and check three things:
- The URL shown in the node starts with
https://n8n.yourdomain.com/webhook/...— if not,WEBHOOK_URLis set incorrectly. - An external call goes through:
curl -i https://n8n.yourdomain.com/webhook/your-pathshould trigger an execution visible in n8n. - A test webhook ("Listen for test event") receives the call: this mode goes through the WebSocket, so it also validates your
Upgrade/Connectiondirectives.
If the production webhook responds but test mode does not, the problem is almost always on the WebSocket side. To go deeper into how webhooks work (test vs production paths, methods, responses), see the complete Webhook node guide.
Nginx, Traefik, or Caddy: when to choose which
All three do the same job; the right choice depends on your context:
- Nginx is the obvious pick when it is already installed on the server and serving other sites: adding a
serverblock for n8n takes five minutes and disturbs nothing. It is also the natural choice if you already know its syntax or your host mandates it. - Caddy is simpler for a single-service server: automatic HTTPS without certbot, a five-line configuration.
- Traefik shines on an all-Docker server with automatic container discovery through labels.
The detailed comparison with matching configurations is in our guide to serving n8n over HTTPS with Traefik or Caddy. The non-negotiable common ground: whatever the proxy, the WEBHOOK_URL and N8N_PROTOCOL variables must be set.
Best practices once in production
- Monitor the instance and the certificate: a healthcheck endpoint behind the proxy and a TLS expiry alert are the bare minimum — our guide to monitoring an n8n instance shows how, including with n8n itself.
- Update carefully: the proxy changes nothing about the procedure, but an unprepared
docker compose pullcan break the instance — follow the method to update n8n without breaking anything. - Back up the database and the encryption key: the Nginx configuration can be rebuilt in ten minutes, your workflows cannot — see the PostgreSQL backup and restore strategy.
- Keep port 5678 private:
127.0.0.1in the Docker port mapping, and a firewall (ufw) that only opens 22, 80, and 443.
Key takeaways
Putting n8n behind Nginx boils down to four steps: bind port 5678 to the loopback interface, obtain the certificate with certbot --nginx, deploy the full server block — with the three WebSocket directives, a raised proxy_read_timeout, and client_max_body_size — then declare the public URL on the n8n side via N8N_PROTOCOL, WEBHOOK_URL, and N8N_EDITOR_BASE_URL. The final test is a webhook in test mode receiving its call: if that works, the editor, real-time executions, and your OAuth integrations will work too. And if your server runs nothing but n8n, take a look at Caddy or Traefik before committing: the best proxy is the one that fits what you already run.
FAQ
Frequently asked questions
Why does the n8n editor show "Connection lost" behind Nginx?
In almost every case, the WebSocket headers are missing from the location block: proxy_http_version 1.1, proxy_set_header Upgrade, and proxy_set_header Connection. The n8n editor and real-time execution view rely on a persistent WebSocket connection; without those three directives, Nginx drops it and the UI keeps disconnecting.
Can I serve n8n from a subdirectory like mysite.com/n8n instead of a subdomain?
Technically yes, via the N8N_PATH variable, but it is a recurring source of trouble: URL rewrites, webhooks, and static assets all need adjusting. A dedicated subdomain (n8n.yourdomain.com) remains the recommended and easiest setup to maintain — a single DNS A record is all it takes.
Does certbot renew the Let's Encrypt certificate automatically?
Yes. On recent distributions, installing certbot sets up a systemd timer (or cron job) that runs certbot renew twice a day and reloads Nginx whenever a certificate was renewed. Verify it works with certbot renew --dry-run; the usual gotcha is a firewall that later blocks port 80.
Should I block direct access to port 5678 once Nginx is in place?
Yes, otherwise anyone can bypass the proxy and reach n8n over unencrypted HTTP. The cleanest approach is to publish the port only on the loopback interface in Docker (127.0.0.1:5678:5678): the port is then reachable only from the server itself, making Nginx the single entry point.
Bundle FlowKit Complet
€269