A client came to us last March with a problem. They’d been running n8n on a cheap VPS from another provider. Open port. Default credentials. No TLS. They didn’t even know it was exposed until someone started using their workflows to send spam through their own SendGrid account. Three days of cleanup. A banned API key. A very angry email from SendGrid’s abuse team.
We helped them rebuild. And honestly, it made us rethink how we talk about self-hosting automation tools. Because n8n is genuinely powerful. But the gap between “it works” and “it’s secure” is wider than most people expect.
So here’s what we’ve learned — after deploying n8n for roughly 80+ clients and handling more migrations than we’d like to count.
Why Self-Host n8n at All?
n8n’s cloud offering is fine for small teams. But if you’re processing sensitive data — customer records, financial info, API credentials for production systems — you don’t want that passing through someone else’s infrastructure. We deal with clients in healthcare, legal, and fintech. Their compliance teams don’t love third-party SaaS touching regulated data.
And there’s the cost angle. One client was paying $240/month for n8n cloud. We moved them to a €12/month VPS. Same workflows. Better performance. Full control.
But control comes with responsibility. That’s where things get interesting.
Step 1: Pick the Right VPS
Don’t cheap out here. We’ve seen people run n8n on 512MB RAM instances and wonder why workflows timeout. Our recommendation: minimum 2 vCPUs, 4GB RAM. If you’re running complex workflows with multiple HTTP nodes hitting external APIs simultaneously, go to 8GB.
Location matters too. If your workflows connect to EU-based services, host in the EU. Latency adds up fast when you’re chaining 15 API calls in sequence. We host everything in the Netherlands for this reason. Low latency to most of Europe. Strong privacy laws. No DMCA nonsense.
One more thing. Use a provider that gives you a dedicated IP. Shared IPs can get blacklisted. We had a case where a client’s outgoing emails kept bouncing because their VPS IP was on three different blocklists before they even deployed n8n.
Step 2: Lock Down SSH Before You Do Anything
Before you even think about Docker or n8n, secure your SSH access. This is non-negotiable.
Disable password authentication.
# /etc/ssh/sshd_config
PasswordAuthentication no
PermitRootLogin no
MaxAuthTries 3
LoginGraceTime 30Use key-based auth only. Ed25519 keys — not RSA. RSA 4096 is fine if you already have it, but ed25519 is shorter and more secure.
ssh-keygen -t ed25519 -C "yourname@hostcreed"Change the default SSH port. Yes, security through obscurity isn’t real security. But it cuts 95% of automated brute-force attacks. We log these — our servers see ~10,000 SSH connection attempts per day on port 22. On a non-standard port? Maybe 50. That’s a real difference.
Install fail2ban. It’s not optional.
apt install fail2ban -y
systemctl enable fail2banConfigure it with a custom jail for SSH. We set bantime to 3600 seconds and maxretry to 3. Aggressive? Maybe. But we’ve never had a breach from SSH.
Step 3: Install Docker Properly
Run n8n in Docker. Always. Don’t install it directly on the host. We learned this the hard way early on — a client’s n8n instance started conflicting with a Node.js version required by another service. Docker isolates everything.
Use Docker Compose. Here’s a production-ready setup we use:
version: '3.8'
services:
n8n:
image: docker.n8n.io/n8nio/n8n:latest
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=admin
- N8N_BASIC_AUTH_PASSWORD=use-a-strong-password-here
- N8N_HOST=yourdomain.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- N8N_ENCRYPTION_KEY=generate-a-random-32-char-string
- WEBHOOK_URL=https://yourdomain.com/
- N8N_JWT_ACTIVATION_SECRET=another-random-string
- GENERIC_TIMEZONE=Europe/Amsterdam
volumes:
- n8n_data:/home/node/.n8n
- ./backups:/backups
networks:
- n8n_net
deploy:
resources:
limits:
memory: 2G
postgres:
image: postgres:15-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=strong-db-password
- POSTGRES_DB=n8n
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- n8n_net
deploy:
resources:
limits:
memory: 1G
volumes:
n8n_data:
postgres_data:
networks:
n8n_net:
driver: bridgeA few things to notice here. The port binding is 127.0.0.1:5678:5678 — not just 5678:5678. This means n8n only listens on localhost. Nobody can hit it from the outside directly. Your reverse proxy handles external traffic.
We’re using PostgreSQL instead of SQLite. SQLite works for testing. For production? PostgreSQL. We had a client corrupt their SQLite database during a power outage. Lost two weeks of workflow history. PostgreSQL handles concurrent writes better and plays nicely with backups.
And those memory limits? They matter. We’ve seen runaway workflows consume 6GB+ RAM. Memory limits keep one bad workflow from crashing your entire VPS.
Step 4: Reverse Proxy with Nginx + TLS
Never expose n8n directly. Use Nginx as a reverse proxy with Let’s Encrypt for TLS.
server {
listen 80;
server_name yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' https: wss: data: 'unsafe-inline' 'unsafe-eval'" always;
# Rate limiting
limit_req_zone $binary_remote_addr zone=n8n:10m rate=10r/s;
location / {
limit_req zone=n8n burst=20 nodelay;
proxy_pass http://127.0.0.1:5678;
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;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}Rate limiting is critical. Without it, anyone can hammer your login page. We had a client who didn’t set this up — their n8n instance received 4,000 login attempts in one hour. Not a compromise, but it maxed out their CPU and killed real workflow execution.
Step 5: Firewall Configuration
Use UFW. Keep it simple.
ufw default deny incoming
ufw default allow outgoing
ufw allow 2222/tcp # Your custom SSH port
ufw allow 80/tcp # HTTP redirect
ufw allow 443/tcp # HTTPS
ufw enableThat’s it. Don’t open port 5678. Don’t open random ports “just in case.” We’ve audited servers where people had 15+ open ports. Why? Nobody could explain. Close everything you don’t need.
Step 6: Automated Backups
This is where most self-hosters fail. They set everything up, run it for months, and then something breaks. No backups. Gone.
We backup three things: the PostgreSQL database, the n8n data volume, and the Docker Compose configuration.
Set up a nightly cron job:
#!/bin/bash
# /opt/scripts/n8n-backup.sh
BACKUP_DIR="/backups"
DATE=$(date +%Y%m%d_%H%M%S)
# Backup PostgreSQL
docker exec postgres pg_dump -U n8n n8n | gzip > "$BACKUP_DIR/db_$DATE.sql.gz"
# Backup n8n data
tar czf "$BACKUP_DIR/n8n_data_$DATE.tar.gz" /var/lib/docker/volumes/n8n_data/
# Keep only last 7 days
find $BACKUP_DIR -type f -mtime +7 -deleteAdd it to crontab:
0 3 * * * /opt/scripts/n8n-backup.shAnd copy backups offsite. We use rsync to push to a separate storage server. Because if your VPS provider has a hardware failure, your local backups are worthless.
Step 7: Monitoring and Updates
Watch your containers. Set up health checks. We use Uptime Kuma on a separate VPS to monitor n8n endpoints. If the service goes down, we get a Telegram alert within 60 seconds.
For updates: don’t auto-update production. We’ve been burned by this. A client had Watchtower running for automatic container updates. An n8n update broke compatibility with three of their workflows. They didn’t notice for two days. Manual updates, tested in staging first. Always.
But do update. Security patches matter. We check for updates weekly and apply them within 48 hours of testing.
My Unpopular Opinion: You Probably Don’t Need n8n
Here’s something most hosting companies won’t tell you. If you’re running fewer than 20 workflows and they’re simple — a webhook here, an API call there, maybe some data transformation — you don’t need n8n. A cron job with a bash script or a simple Python script would do the job with zero attack surface.
We’ve onboarded clients who moved to n8n because it was trendy. They had five workflows that could’ve been shell scripts. And now they’re managing a full Docker stack with a database and a web interface that needs patching and monitoring. For what? A visual workflow builder?
n8n shines when you have complex, multi-branch workflows with error handling, retries, and human-in-the-loop approvals. If you’re not doing that, you’re adding infrastructure complexity for no reason. We tell clients this. Some listen. Some don’t. The ones who listen save money and sleep better.
What We Got Wrong Early On
When we first started hosting n8n for clients, we didn’t isolate workflows by user. Everything ran in one container. One client’s buggy workflow — an infinite loop hitting a rate-limited API — brought down n8n for everyone on that server. Took us three hours to figure out what happened.
Now we run separate n8n instances per client. It uses more resources. But the isolation is worth it. If one client breaks something, it’s their problem. Not everyone’s.
Final Thoughts
Self-hosting n8n isn’t hard. But it’s not trivial either. The gap is in the details — the SSH hardening, the firewall rules, the backup automation, the monitoring. Skip any one of those and you’re building on sand.
We’ve been doing this for years. Across hundreds of deployments, the clients who follow these steps — every single one — have never had a security incident. The ones who skip steps? We hear from them eventually. Usually in a panic.
Do it right the first time. Your future self will thank you. And so will your clients.