Introduction
Self-hosting n8n gives you unlimited workflow executions, complete data control, and no per-task costs. But setting up a secure, accessible deployment requires handling networking, SSL, and domain configuration. Cloudflare Tunnel provides an elegant solution—creating a secure connection from your server to Cloudflare’s network without exposing any ports to the public internet.
This guide walks through deploying n8n with Docker, securing it with Cloudflare Tunnel, and configuring a custom domain with automatic SSL.
Why Cloudflare Tunnel?
Traditional self-hosting requires opening ports on your firewall and managing SSL certificates. Cloudflare Tunnel (formerly Argo Tunnel) eliminates these requirements:
- No open ports: The tunnel creates an outbound connection from your server to Cloudflare
- Automatic SSL: Cloudflare handles SSL/TLS certificates
- DDoS protection: Cloudflare’s network protects your instance
- No IP exposure: Your server’s IP address is never exposed
- Easy custom domains: Configure domains through Cloudflare DNS
Prerequisites
Before starting, you need:
- A server (VPS, dedicated server, or local machine) with Docker installed
- A Cloudflare account with a registered domain
- Basic familiarity with Docker and command-line tools
Step 1: Deploy n8n with Docker
Create the Docker Compose Configuration
# docker-compose.yml
version: '3.8'
services:
n8n:
image: n8nio/n8n:latest
restart: always
ports:
- "127.0.0.1:5678:5678"
environment:
# Basic configuration
- N8N_HOST=n8n.yourdomain.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- N8N_EDITOR_BASE_URL=https://n8n.yourdomain.com
- WEBHOOK_URL=https://n8n.yourdomain.com/
# Security
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER}
- N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD}
# Database (PostgreSQL for production)
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=${POSTGRES_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
# Execution settings
- EXECUTIONS_DATA_PRUNE=true
- EXECUTIONS_DATA_MAX_AGE=168
- N8N_METRICS=true
# Timezone
- GENERIC_TIMEZONE=America/New_York
volumes:
- n8n_data:/home/node/.n8n
depends_on:
- postgres
postgres:
image: postgres:16-alpine
restart: always
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
# Don't expose ports - only accessible within Docker network
volumes:
n8n_data:
postgres_data:
Create the Environment File
# .env file
N8N_ENCRYPTION_KEY=generate-a-random-32-char-string-here
N8N_BASIC_AUTH_USER=admin
N8N_BASIC_AUTH_PASSWORD=your-secure-password
POSTGRES_USER=n8n
POSTGRES_PASSWORD=your-secure-db-password
Generate a secure encryption key:
openssl rand -hex 32
Start the Services
# Start n8n and PostgreSQL
docker compose up -d
# Check that services are running
docker compose ps
# View logs
docker compose logs -f n8n
n8n should now be running on localhost:5678. Note that we bound the port to 127.0.0.1 only, so it’s not accessible from outside the server.
Step 2: Install and Configure Cloudflare Tunnel
Install cloudflared
# On Ubuntu/Debian
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
sudo dpkg -i cloudflared.deb
# Verify installation
cloudflared --version
Authenticate with Cloudflare
cloudflared tunnel login
This opens a browser window to authenticate with your Cloudflare account. Select the domain you want to use for n8n.
Create a Tunnel
# Create a named tunnel
cloudflared tunnel create n8n-tunnel
# This creates a tunnel with a unique ID and a credentials file
# Note the tunnel ID and credentials file path
Configure the Tunnel
Create a configuration file for the tunnel:
# ~/.cloudflared/config.yml
tunnel: <your-tunnel-id>
credentials-file: /root/.cloudflared/<your-tunnel-id>.json
ingress:
- hostname: n8n.yourdomain.com
service: http://localhost:5678
- service: http_status:404
Create DNS Record
# Create a CNAME record pointing to the tunnel
cloudflared tunnel route dns n8n-tunnel n8n.yourdomain.com
This automatically creates a CNAME record in your Cloudflare DNS pointing n8n.yourdomain.com to the tunnel.
Test the Tunnel
# Run the tunnel in the foreground to test
cloudflared tunnel run n8n-tunnel
Open a browser and navigate to https://n8n.yourdomain.com. You should see the n8n setup page.
Step 3: Run Cloudflare Tunnel as a Service
Create a Systemd Service
# /etc/systemd/system/cloudflared.service
[Unit]
Description=Cloudflare Tunnel for n8n
After=network.target
[Service]
TimeoutStartSec=0
Type=notify
ExecStart=/usr/bin/cloudflared tunnel --config /root/.cloudflared/config.yml run n8n-tunnel
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
Enable and Start the Service
# Reload systemd
sudo systemctl daemon-reload
# Enable the service to start on boot
sudo systemctl enable cloudflared
# Start the service
sudo systemctl start cloudflared
# Check status
sudo systemctl status cloudflared
Step 4: Configure Cloudflare Settings
SSL/TLS Settings
In your Cloudflare dashboard:
- Navigate to SSL/TLS settings
- Set encryption mode to Full (not Flexible, which can cause redirect loops)
- Enable Always Use HTTPS
- Enable Automatic HTTPS Rewrites
Access Policies (Optional)
Restrict access to specific IP addresses or require Cloudflare authentication:
# In Cloudflare Zero Trust dashboard
# Access > Applications > Add Application
# Type: Self-hosted
# Domain: n8n.yourdomain.com
# Policy:
# - Allow specific emails or email domains
# - Or allow specific IP ranges
Rate Limiting
Protect your n8n instance from abuse:
# Cloudflare Rate Limiting Rule
# If: requests to n8n.yourdomain.com/*
# Then: rate limit to 100 requests per 10 seconds per IP
# Action: Block for 60 seconds
Step 5: Security Best Practices
Secure the Docker Deployment
# Add to docker-compose.yml for additional security
services:
n8n:
# Run as non-root user
user: "node:node"
# Read-only filesystem where possible
read_only: false
# Drop all capabilities
cap_drop:
- ALL
# No new privileges
security_opt:
- no-new-privileges:true
# Resource limits
deploy:
resources:
limits:
memory: 1G
cpus: '1.0'
Environment Security
# Secure the .env file
chmod 600 .env
chown root:root .env
# Regularly rotate the encryption key (requires re-encrypting credentials)
# Back up the .env file securely
Database Security
# PostgreSQL security in docker-compose.yml
services:
postgres:
# Don't expose any ports
# Only accessible within Docker network
# Add health check
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n"]
interval: 10s
timeout: 5s
retries: 5
Regular Backups
#!/bin/bash
# backup-n8n.sh - Run daily via cron
BACKUP_DIR="/backups/n8n"
DATE=$(date +%Y%m%d_%H%M%S)
# Backup PostgreSQL
docker compose exec -T postgres pg_dump -U n8n n8n | gzip > \
"$BACKUP_DIR/db_$DATE.sql.gz"
# Backup n8n data volume
docker run --rm -v n8n_data:/data -v $BACKUP_DIR:/backup \
alpine tar czf /backup/n8n_data_$DATE.tar.gz -C /data .
# Keep only last 30 days of backups
find $BACKUP_DIR -type f -mtime +30 -delete
# Add to crontab
0 2 * * * /path/to/backup-n8n.sh
Step 6: Monitoring and Maintenance
Health Checks
#!/bin/bash
# health-check.sh
# Check if n8n is responding
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5678)
if [ "$HTTP_STATUS" != "200" ] && [ "$HTTP_STATUS" != "302" ]; then
echo "n8n is down! HTTP status: $HTTP_STATUS"
# Send alert
curl -X POST "https://alerts.example.com/api" \
-d "message=n8n is down (HTTP $HTTP_STATUS)"
# Try to restart
docker compose restart n8n
fi
# Check if tunnel is running
if ! systemctl is-active --quiet cloudflared; then
echo "Cloudflare tunnel is down!"
systemctl restart cloudflared
fi
Update n8n
# Pull the latest image
docker compose pull
# Recreate containers with the new image
docker compose up -d
# Verify the update
docker compose logs n8n | tail -20
Monitor Resource Usage
# Check Docker resource usage
docker stats
# Check disk space
df -h
# Check Docker volume sizes
docker system df -v
Troubleshooting
Common Issues
n8n shows “connection refused”: Ensure n8n is running and the port is correct in the tunnel config.
Redirect loops: Set Cloudflare SSL mode to “Full” not “Flexible”.
Webhooks not working: Ensure WEBHOOK_URL is set correctly to your public URL.
Tunnel disconnects: Check the cloudflared service logs and ensure your server has a stable internet connection.
# Check tunnel logs
journalctl -u cloudflared -f
# Check n8n logs
docker compose logs -f n8n
Conclusion
Deploying n8n with Cloudflare Tunnel provides a secure, self-hosted automation platform without exposing your server to the public internet. The combination of Docker for containerization, PostgreSQL for reliable data storage, and Cloudflare Tunnel for secure access creates a production-ready setup that costs only a few dollars per month in hosting.
The key advantages—no open ports, automatic SSL, DDoS protection, and unlimited workflow executions—make this setup ideal for individuals and small teams who want the power of n8n without the limitations of cloud plans. With proper backups, monitoring, and security configuration, your self-hosted n8n instance will run reliably and securely for the long term.