How to Deploy n8n with Docker on Any VPS (2026 Guide)
What You’ll Need
- n8n Cloud or self-hosted n8n (we’re covering self-hosted)
- Hetzner VPS , Contabo VPS , or DigitalOcean
- A domain name (optional, but recommended—grab one at Namecheap )
- Basic command-line comfort
- 2GB+ RAM and 20GB+ storage on your VPS
Table of Contents
- Why Self-Host n8n on a VPS?
- Setting Up Your VPS
- Installing Docker
- Deploying n8n with Docker Compose
- Configuring SSL and Your Domain
- Running Your First Workflow
- Getting Started with Production
Why Self-Host n8n on a VPS?
I made the switch to self-hosted n8n about two years ago, and honestly, it changed how I build automation. You get complete control over your workflows, no per-execution pricing limits, and the ability to run custom code without restrictions. Plus, if you’re already paying for a VPS for automation , you can run n8n alongside other services—databases, APIs, webhooks—all in one place.
The deployment is surprisingly straightforward with Docker. I’ll walk you through it step-by-step so you can have a production-ready n8n instance running in under an hour.
Setting Up Your VPS
Start with a fresh Ubuntu 22.04 LTS instance. If you’re comparing hosting options, I’ve got a detailed breakdown of VPS choices for automation (Hetzner vs Contabo vs Railway) that might help you decide. For this guide, I’m assuming you’ve already provisioned your server and can SSH into it.
Connect to your VPS:
ssh root@your_vps_ip_address
Update your system packages:
apt update && apt upgrade -y
Create a non-root user for security (optional but recommended):
adduser n8nuser
usermod -aG sudo n8nuser
su - n8nuser
Installing Docker
Docker is the easiest way to run n8n without dependency hell. Install the official Docker repository:
sudo apt install -y ca-certificates curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Install Docker and Docker Compose:
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Verify the installation:
docker --version
docker compose version
Add your user to the Docker group so you don’t need sudo every time:
sudo usermod -aG docker $USER
newgrp docker
Deploying n8n with Docker Compose
Create a project directory:
mkdir -p ~/n8n-deployment
cd ~/n8n-deployment
Create a .env file for configuration:
cat > .env << 'EOF'
N8N_HOST=your-domain.com
N8N_PORT=5678
N8N_PROTOCOL=https
NODE_ENV=production
N8N_SECURE_COOKIE=true
N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)
GENERIC_TIMEZONE=UTC
DB_TYPE=postgres
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n
DB_POSTGRESDB_PASSWORD=$(openssl rand -hex 16)
WEBHOOK_URL=https://your-domain.com/webhook
EOF
Replace your-domain.com with your actual domain. If you don’t have a domain yet, grab one from Namecheap
for cheap.
Now create the docker-compose.yml file:
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: n8n-postgres
environment:
POSTGRES_USER: ${DB_POSTGRESDB_USER}
POSTGRES_PASSWORD: ${DB_POSTGRESDB_PASSWORD}
POSTGRES_DB: ${DB_POSTGRESDB_DATABASE}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- n8n-net
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_POSTGRESDB_USER}"]
interval: 10s
timeout: 5s
retries: 5
n8n:
image: n8nio/n8n:latest
container_name: n8n-app
restart: unless-stopped
ports:
- "5678:5678"
environment:
- DB_TYPE=${DB_TYPE}
- DB_POSTGRESDB_HOST=${DB_POSTGRESDB_HOST}
- DB_POSTGRESDB_PORT=${DB_POSTGRESDB_PORT}
- DB_POSTGRESDB_DATABASE=${DB_POSTGRESDB_DATABASE}
- DB_POSTGRESDB_USER=${DB_POSTGRESDB_USER}
- DB_POSTGRESDB_PASSWORD=${DB_POSTGRESDB_PASSWORD}
- N8N_HOST=${N8N_HOST}
- N8N_PORT=${N8N_PORT}
- N8N_PROTOCOL=${N8N_PROTOCOL}
- NODE_ENV=${NODE_ENV}
- N8N_SECURE_COOKIE=${N8N_SECURE_COOKIE}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- WEBHOOK_URL=${WEBHOOK_URL}
volumes:
- n8n_data:/home/node/.n8n
networks:
- n8n-net
depends_on:
postgres:
condition: service_healthy
nginx:
image: nginx:alpine
container_name: n8n-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
networks:
- n8n-net
depends_on:
- n8n
volumes:
postgres_data:
n8n_data:
networks:
n8n-net:
driver: bridge
EOF
Create the Nginx configuration file:
cat > nginx.conf << 'EOF'
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
types_hash_max_size 2048;
gzip on;
upstream n8n {
server n8n:5678;
}
server {
listen 80;
server_name your-domain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
client_max_body_size 50M;
location / {
proxy_pass http://n8n;
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-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
}
EOF
💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.
Configuring SSL and Your Domain
First, point your domain to your VPS IP address in your DNS provider. Create an SSL certificate using Let’s Encrypt:
sudo apt install -y certbot python3-certbot-nginx
sudo mkdir -p ssl
cd ssl
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem -subj "/CN=your-domain.com"
cd ..
For production, use Let’s Encrypt instead:
sudo certbot certonly --standalone -d your-domain.com --non-interactive --agree-tos -m your-email@example.com
sudo cp /etc/letsencrypt/live/your-domain.com/fullchain.pem ssl/cert.pem
sudo cp /etc/letsencrypt/live/your-domain.com/privkey.pem ssl/key.pem
sudo chown -R $USER:$USER ssl/
Update your nginx.conf with your actual domain name (replace your-domain.com twice in the file).
Start the deployment:
docker compose up -d
Check that all containers are running:
docker compose ps
You should see three services: n8n-postgres, n8n-app, and n8n-nginx. View logs if anything fails:
docker compose logs -f n8n-app
Running Your First Workflow
Open your browser and go to https://your-domain.com. You’ll see the n8n setup screen. Create your admin account and log in.
Once inside, create a simple test workflow to verify everything works. Click the + button to add a trigger node. Select Webhook:
{
"method": "POST",
"path": "webhook/test"
}
Add an HTTP Request node:
Click the + button again and search for “HTTP Request”. Configure it like this:
{
"method": "GET",
"url": "https://jsonplaceholder.typicode.com/posts/1",
"authentication": "None"
}
Connect the webhook to the HTTP Request node by dragging the output handle. Add a final node to return the response—search for “Respond to Webhook”:
{
"statusCode": 200,
"responseBody": "={{ $json }}"
}
Connect the HTTP Request output to the Respond to Webhook input. Now test it by clicking the Execute Workflow button. You should see the POST data flow through, hit the external API, and return a response. That’s it—your workflow engine is live.
To actually trigger this via HTTP, copy the webhook URL from the Webhook node and make a POST request:
curl -X POST https://your-domain.com/webhook/test \
-H "Content-Type: application/json" \
-d '{"message":"test"}'
From here, you can connect real services. Need to send Slack notifications when something happens? Drop in a Slack node. Want to sync data between databases? Add a PostgreSQL node. The n8n node library has hundreds of integrations built-in, and you can write custom JavaScript in the Function node for anything else.
Getting Started
You now have a production-ready n8n instance running on your own infrastructure. Here’s what you’ve got:
- Self-hosted n8n with PostgreSQL persistence—your workflows and execution history are safe
- Nginx reverse proxy with SSL—encrypt all traffic to and from your instance
- Docker Compose for easy updates and backups—upgrading n8n is one command:
docker compose pull && docker compose up -d
If you hit snags during setup, the n8n docs cover edge cases well. For quick integrations, start with webhook triggers and HTTP requests—those are the bread and butter of workflow automation.
Want to scale this further? You can add more VPS instances behind a load balancer, set up automated backups of your n8n database, or integrate with your CI/CD pipeline to version-control your workflows. The beauty of self-hosting is you own the entire stack.
Outsource Your Automation
Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr — mention SYS3-HUGO for priority. Or DM at chasebot.online .
Want to automate this yourself?
Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.
Want this engine running on your own VPS?
This blog publishes itself — daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.
Get System 3