Self-hosting open source tools on budget VPS
What You’ll Need
- n8n Cloud or self-hosted n8n instance
- Hetzner VPS or Contabo VPS for hosting ($3-8/month)
- DigitalOcean as an alternative ($4/month Droplet)
- Namecheap for domain registration (if needed)
- SSH client (built into macOS/Linux, PuTTY for Windows)
- Basic Linux command-line knowledge
- Docker installed on your VPS (we’ll cover this)
Table of Contents
- Why Self-Host Open Source Tools
- Choosing the Right Budget VPS
- Setting Up Your Server Foundation
- Installing and Configuring n8n
- Running PostgreSQL for Data Storage
- Securing Your Self-Hosted Stack
- Monitoring and Maintenance
- Getting Started
Why Self-Host Open Source Tools
I’ve spent the last three years building automation workflows, and the cost difference between SaaS and self-hosted solutions is staggering. When you’re running dozens of workflows daily, platform fees add up fast. Self-hosting open source tools like n8n gives you complete control, unlimited automation runs, and data stored on your infrastructure.
The trade-off? You manage the server. But on a $5/month VPS, that’s still a massive win compared to $29/month per seat on managed platforms.
For context, if you’re deciding between workflow automation solutions, I’ve compared the actual costs of Temporal vs n8n vs Zapier API Automation Costs in another guide—self-hosting n8n consistently wins for small-to-medium teams.
Choosing the Right Budget VPS
I’ve tested hosting on Hetzner VPS , Contabo VPS , and DigitalOcean . Here’s what actually matters:
Hetzner CPX11 ($5.29/month): 2 vCPU, 4GB RAM, 40GB SSD
- Best value. Runs n8n + PostgreSQL smoothly.
- Datacenter in Europe; excellent uptime.
- Slightly slower dashboard than competitors.
Contabo VPS S ($3.99/month): 4 vCPU, 8GB RAM, 200GB SSD
- Marketed aggressively, but oversold during peak hours.
- Fine for hobby projects; I’d upgrade for production.
DigitalOcean Droplet ($4/month): 512MB RAM, 1 vCPU, 10GB SSD
- Too small for n8n + database. Upgrade to $6/month tier ($1GB RAM).
- Premium support and one-click installs reduce headaches.
My pick: Hetzner VPS for production, DigitalOcean for learning. Both have excellent uptime (99.9%+).
For your domain, grab it from Namecheap —$0.88/year for .xyz domains, and they’re good at DNS management.
Setting Up Your Server Foundation
I’ll walk you through deploying on a fresh Ubuntu 22.04 LTS instance. This setup takes 20 minutes if you follow exactly.
Step 1: SSH into your server
ssh root@your_vps_ip_address
Replace your_vps_ip_address with the actual IP. Your hosting provider emails this at signup.
Step 2: Update system packages
apt update
apt upgrade -y
This patches security vulnerabilities—critical for production.
Step 3: Install Docker and Docker Compose
apt install -y curl gnupg2 lsb-release ubuntu-keyring
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Verify installation:
docker --version
docker compose version
Step 4: Create a non-root user (security best practice)
useradd -m -s /bin/bash automation
usermod -aG docker automation
su - automation
Docker commands now work without sudo. This prevents accidental root-level damage.
Installing and Configuring n8n
n8n is my workflow engine of choice—1400+ pre-built integrations, visual editor, and bulletproof reliability.
💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.
Step 1: Create project directory
cd ~
mkdir n8n-deployment
cd n8n-deployment
Step 2: Create docker-compose.yml
I’m using PostgreSQL for persistence (SQLite corrupts on larger deployments).
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: n8n-postgres
restart: always
environment:
POSTGRES_DB: n8n
POSTGRES_USER: n8n
POSTGRES_PASSWORD: secure_password_change_this_12345
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n"]
interval: 10s
timeout: 5s
retries: 5
n8n:
image: n8nio/n8n:latest
container_name: n8n
restart: always
depends_on:
postgres:
condition: service_healthy
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: 5432
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: secure_password_change_this_12345
N8N_PROTOCOL: https
N8N_HOST: your_domain.com
N8N_PORT: 443
N8N_SECURE_COOKIE: "true"
N8N_EDITOR_BASE_URL: https://your_domain.com/
WEBHOOK_TUNNEL_URL: https://your_domain.com/
NODE_ENV: production
NODE_OPTIONS: --max-old-space-size=1024
volumes:
- n8n_data:/home/node/.n8n
ports:
- "5678:5678"
networks:
- n8n-network
nginx:
image: nginx:alpine
container_name: n8n-nginx
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- n8n
networks:
- n8n-network
volumes:
postgres_data:
n8n_data:
networks:
n8n-network:
driver: bridge
Replace secure_password_change_this_12345 with a 32-character random string:
openssl rand -base64 32
Replace your_domain.com with your actual domain.
Step 3: Create nginx.conf
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;
keepalive_timeout 65;
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_read_timeout 3600s;
proxy_connect_timeout 3600s;
proxy_send_timeout 3600s;
}
}
}
EOF
Running PostgreSQL for Data Storage
PostgreSQL handles persistence better than SQLite, especially when scaling. The docker-compose setup above includes it, but here’s the manual approach if you prefer:
Step 1: Start PostgreSQL
docker run -d \
--name n8n-postgres \
--restart always \
-e POSTGRES_DB=n8n \
-e POSTGRES_USER=n8n \
-e POSTGRES_PASSWORD=secure_password_change_this_12345 \
-v postgres_data:/var/lib/postgresql/data \
postgres:15-alpine
Step 2: Verify it’s running
docker ps
docker logs n8n-postgres
You should see “database system is ready to accept connections.”
Securing Your Self-Hosted Stack
This is non-negotiable. Open-source tools on public
Want to automate this yourself?
Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.