How to Self-Host n8n on Your VPS (Simple, Secure, and Production-Ready)
Docker deployment secured with Cloudflare Tunnel, hardened access, and production-ready tweaks

🐳 Docker & DevOps Implementation Guides
Complete Docker guides with optimization techniques, deployment strategies, and automation prompts to streamline your containerization workflow.
If you’ve been using n8n Cloud but want full control of your automations (and to stop paying monthly fees), self-hosting n8n on your own VPS is a great move.
In this guide, I’ll show you how to install n8n on any VPS using Docker, expose it securely to the public internet using Cloudflare Tunnel (no Nginx, no Let’s Encrypt, no open ports), and configure it for production with your real domain.
Why Self-Host n8n?
Running n8n yourself gives you:
- Full data ownership — credentials and workflows stay on your server
- Unlimited workflows for a fixed VPS cost
- Better integration with private systems and APIs
- More control over scaling, logging, and automation triggers
Even a small 2 GB VPS can handle dozens of n8n workflows comfortably.
1. Set Up Your VPS
You can use any provider — Hetzner, DigitalOcean, AWS Lightsail, etc. Make sure it’s running Ubuntu 22.04 LTS or newer.
Update and install Docker:
sudo apt update && sudo apt upgrade -y
sudo apt install docker.io curl -y
sudo systemctl enable --now docker
Verify Docker is running:
docker ps
2. Run n8n in Docker
Create a Docker volume to persist data:
docker volume create n8n_data
Start n8n for the first time:
docker run -d \
--name n8n \
-p 5678:5678 \
-e GENERIC_TIMEZONE="CET" \
-v n8n_data:/home/node/.n8n \
docker.n8n.io/n8nio/n8n
At this point, n8n is running locally on port 5678. Instead of exposing that port to the public, we’ll secure it through Cloudflare.
3. Expose n8n Securely with Cloudflare Tunnel
You could use Nginx and Let’s Encrypt manually, but Cloudflare Tunnel makes it simpler and safer — no ports, no certificates, and automatic HTTPS.
If you want a detailed explanation of how tunnels work and why they replace reverse proxies, check out: Expose Any Docker Container to the Public with Cloudflare Tunnel (No Nginx, No Open Ports)
Install cloudflared
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bullseye main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install cloudflared -y
Authenticate with Cloudflare
cloudflared login
Choose your domain and authorize through the browser.
Credentials are saved at ~/.cloudflared/cert.pem
.
Create a Named Tunnel
cloudflared tunnel create n8n-tunnel
Route a Subdomain
For example, route automate.we-hate-copy-pasting.com:
cloudflared tunnel route dns n8n-tunnel automate.we-hate-copy-pasting.com
4. Start n8n Again with Your Correct Domain
Now that your tunnel and domain are ready, restart n8n with the proper production configuration.
docker stop n8n && docker rm n8n
docker run -d \
--name n8n \
-p 5678:5678 \
-e GENERIC_TIMEZONE="CET" \
-e TZ="CET" \
-e N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true \
-e N8N_RUNNERS_ENABLED=true \
-e WEBHOOK_URL="https://automate.we-hate-copy-pasting.com/" \
-v n8n_data:/home/node/.n8n \
docker.n8n.io/n8nio/n8n
The WEBHOOK_URL
variable ensures that external triggers, webhooks, and integrations use your real HTTPS domain — essential for production deployments.
5. Connect the Tunnel to n8n
Run this to connect the Cloudflare Tunnel to your local n8n instance:
cloudflared tunnel --url http://localhost:5678 run n8n-tunnel
After a few seconds, you can open:
https://automate.we-hate-copy-pasting.com
You’ll see the n8n editor live, served securely through Cloudflare.
6. Keep the Tunnel Running with systemd
Create a persistent configuration file.
File: ~/.cloudflared/config.yml
tunnel: n8n-tunnel
credentials-file: /home/ubuntu/.cloudflared/<tunnel-id>.json
ingress:
- hostname: automate.we-hate-copy-pasting.com
service: http://localhost:5678
- service: http_status:404
Then create a systemd service:
File: /etc/systemd/system/cloudflared-n8n.service
[Unit]
Description=Cloudflared Tunnel for n8n
After=network.target
[Service]
User=ubuntu
ExecStart=/usr/bin/cloudflared --config /home/ubuntu/.cloudflared/config.yml tunnel run n8n-tunnel
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Enable and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared-n8n.service
Check status:
sudo systemctl status cloudflared-n8n
Now your tunnel reconnects automatically after reboots or connection drops.
7. Add Basic Authentication
Protect your n8n dashboard by enabling Basic Auth:
docker stop n8n && docker rm n8n
docker run -d \
--name n8n \
-p 5678:5678 \
-e GENERIC_TIMEZONE="CET" \
-e TZ="CET" \
-e WEBHOOK_URL="https://automate.we-hate-copy-pasting.com/" \
-e N8N_BASIC_AUTH_ACTIVE=true \
-e N8N_BASIC_AUTH_USER=admin \
-e N8N_BASIC_AUTH_PASSWORD=strongpassword \
-v n8n_data:/home/node/.n8n \
docker.n8n.io/n8nio/n8n
Now you’ll be prompted for credentials before accessing the UI.
8. (Optional) Use PostgreSQL for Production
SQLite is fine for small setups, but PostgreSQL is better for reliability and multi-user workflows. You can easily launch a Dockerized database right next to n8n — I’ve written a separate article for that: How to Quickly Launch a Docker Instance of PostgreSQL on Your VPS
Then update your n8n container with:
-e DB_TYPE=postgresdb \ -e DB_POSTGRESDB_DATABASE=n8n \ -e DB_POSTGRESDB_HOST=localhost \ -e DB_POSTGRESDB_PORT=5432 \ -e DB_POSTGRESDB_USER=n8nuser \ -e DB_POSTGRESDB_PASSWORD=supersecret \
9. Verify Everything Works
Visit your domain:
https://automate.we-hate-copy-pasting.com
You should see your self-hosted n8n instance — encrypted, authenticated, and backed by Cloudflare’s network. No open ports, no Nginx, no manual certificates.
10. Conclusion
You now have your own production-ready n8n Cloud running on your VPS:
- Docker handles isolation and persistence
- Cloudflare Tunnel provides secure HTTPS without opening ports
WEBHOOK_URL
ensures all workflows use your real domain- systemd keeps it online automatically
- PostgreSQL (optional) prepares it for scale
You fully own your automation stack — and you can reuse this exact setup for any self-hosted service.
If you want to dive deeper into the Cloudflare setup or connect a database next, check out:
- Expose Any Docker Container to the Public with Cloudflare Tunnel (No Nginx, No Open Ports)
- How to Quickly Launch a Docker Instance of PostgreSQL on Your VPS
Thanks, Matija