What You'll Need
- n8n Cloud or self-hosted n8n
- Hetzner VPS or Contabo VPS for hosting
- Namecheap if domain needed
- DigitalOcean as alternative
- Make.com only for comparisons
Table of Contents
- Provisioning the Hetzner Server Architecture
- Deploying n8n with Docker Compose and PostgreSQL
- Configuring Caddy Reverse Proxy and SSL
- Monitoring, Log Aggregation, and Automated Backups
- Getting Started
Provisioning the Hetzner Server Architecture
When setting up self-hosted workflow automation, infrastructure costs and reliability are the main considerations. Proprietary SaaS platforms become expensive as execution volumes scale into tens of thousands of tasks per month. Running an open-source workflow execution engine on a cloud server gives you uncapped execution capacity at a fixed monthly price.
I prefer using a cloud instance on Hetzner VPS for this work. A CX22 or CX32 instance provides dedicated vCPU resources, NVMe storage, and high-bandwidth networking at a fraction of the cost of legacy providers. If you need alternative infrastructure, providers like DigitalOcean or Contabo VPS work similarly, but Hetzner offers unmatched price-to-performance metrics in European and North American datacenters.
To begin, launch an Ubuntu 22.04 LTS instance on Hetzner. Once the server boots, connect via SSH and execute the initial system updates and firewall setup.
sudo apt-get update && sudo apt-get upgrade -y
sudo apt-get install -y curl git ufw jq CA-certificates gnupg
Next, configure the Uncomplicated Firewall (UFW) to enforce a strict security profile. Block all incoming ports except standard SSH, HTTP, and HTTPS traffic.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Now, install the latest official Docker Engine and the Docker Compose plugin directly from Docker's upstream repository.
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Verify that the Docker daemon is active and configured to launch on boot.
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
Create a designated working directory on your Hetzner host where your deployment stacks and configuration files will reside.
mkdir -p /opt/automation-stack/caddy_data
mkdir -p /opt/automation-stack/caddy_config
mkdir -p /opt/automation-stack/postgres_data
mkdir -p /opt/automation-stack/n8n_data
cd /opt/automation-stack
Deploying n8n with Docker Compose and PostgreSQL
While SaaS options like Make.com offer managed convenience, running self-hosted n8n Cloud alternative software gives you full control over data privacy, execution limits, and custom integrations.
For production workloads, running n8n with an embedded SQLite database is insufficient due to lock contention during parallel workflow executions. We will deploy n8n backed by a robust PostgreSQL container.
To ensure container stability and automatic recovery, we integrate health checks directly into the orchestration file. If you want to dive deeper into container readiness, review our guide on Configuring Docker Compose Container Health Checks.
Create the docker-compose.yml file in /opt/automation-stack/docker-compose.yml:
version: '3.8'
networks:
automation_net:
driver: bridge
volumes:
postgres_storage:
n8n_storage:
caddy_data:
caddy_config:
services:
postgres:
image: postgres:15-alpine
container_name: postgres_db
restart: always
networks:
- automation_net
environment:
POSTGRES_USER: n8n_db_user
POSTGRES_PASSWORD: StrongProductionPassword987!
POSTGRES_DB: n8n_database
volumes:
- postgres_storage:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n_db_user -d n8n_database"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
n8n:
image: docker.n8n.io/n8nio/n8n:latest
container_name: n8n_engine
restart: always
networks:
- automation_net
ports:
- "127.0.0.1:5678:5678"
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n_database
- DB_POSTGRESDB_USER=n8n_db_user
- DB_POSTGRESDB_PASSWORD=StrongProductionPassword987!
- N8N_HOST=automation.example.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- NODE_ENV=production
- WEBHOOK_URL=https://automation.example.com/
- EXECUTIONS_DATA_PRUNE=true
- EXECUTIONS_DATA_MAX_AGE=168
- EXECUTIONS_DATA_PRUNE_MAX_COUNT=50000
- GENERIC_TIMEZONE=UTC
volumes:
- n8n_storage:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:5678/healthz"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
caddy:
image: caddy:2-alpine
container_name: caddy_proxy
restart: always
networks:
- automation_net
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- n8n
Start the container ecosystem in detached mode:
docker compose up -d
Check the startup status of the container stack:
docker compose ps
💡 Fast-Track Your Project: Don't want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.
Configuring Caddy Reverse Proxy and SSL
With the services defined, we must safely route public web traffic to our n8n instance using encrypted TLS tunnels. You can purchase your base domain through Namecheap and point an A Record containing your host's public IP address directly to your server (for example, automation.example.com).
We use Caddy as our reverse proxy because it handles Automatic Certificate Management Environment (ACME) challenges seamlessly, provision TLS certificates from Let's Encrypt without external cron jobs or Certbot scripts.
Create the configuration file at /opt/automation-stack/Caddyfile:
automation.example.com {
encode gzip zstd
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
X-XSS-Protection "1; mode=block"
Referrer-Policy "no-referrer-when-downgrade"
}
reverse_proxy n8n:5678 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
Reload the running Caddy service to pick up the updated directives:
docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile
You can now test your domain in a Web browser. Caddy automatically provisions standard TLS certificates and transparently proxies traffic down to n8n over the internal Docker network (automation_net).
Monitoring, Log Aggregation, and Automated Backups
A production deployment requires log aggregation and back-up schedules for disaster recovery. If you are operating multiple services, review our full guide on Configuring Centralized Log Aggregation with Grafana Loki to ship your container outputs into centralized dashboards.
Creating an Automated PostgreSQL Backup Script
We will write a robust Python utility that connects to our database container, executes a database dump, compresses the output file, and enforces data retention by deleting files older than 14 days.
Create the backup script file at /opt/automation-stack/db_backup.py:
import os
import subprocess
import datetime
import glob
BACKUP_DIR = "/opt/automation-stack/backups"
CONTAINER_NAME = "postgres_db"
DB_USER = "n8n_db_user"
DB_NAME = "n8n_database"
RETENTION_DAYS = 14
def run_backup():
if not os.path.exists(BACKUP_DIR):
os.makedirs(BACKUP_DIR, mode=0o750)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"n8n_backup_{timestamp}.sql.gz"
filepath = os.path.join(BACKUP_DIR, filename)
dump_cmd = (
f"docker exec {CONTAINER_NAME} pg_dump -U {DB_USER} {DB_NAME} | gzip > {filepath}"
)
print(f"Starting backup: {filepath}")
result = subprocess.run(dump_cmd, shell=True, capture_output=True, text=True)
if result.returncode == 0:
print("Database dump successfully created.")
else:
print(f"Error executing database dump: {result.stderr}")
return
cleanup_old_backups()
def cleanup_old_backups():
now = datetime.datetime.now()
pattern = os.path.join(BACKUP_DIR, "n8n_backup_*.sql.gz")
backup_files = glob.glob(pattern)
for file_path in backup_files:
file_time = datetime.datetime.fromtimestamp(os.path.getmtime(file_path))
age_days = (now - file_time).days
if age_days > RETENTION_DAYS:
try:
os.remove(file_path)
print(f"Removed expired backup: {file_path}")
except OSError as e:
print(f"Failed to remove file {file_path}: {e}")
if __name__ == "__main__":
run_backup()
Make the script executable:
chmod +x /opt/automation-stack/db_backup.py
Automation via Systemd Service and Timer
Instead of relying on legacy cron daemons, we schedule our Python script using Systemd units for improved monitoring and process isolation. To learn more about native Linux scheduling, read our guide on How to Schedule Python Scripts With Systemd.
Create the systemd service unit file at /etc/systemd/system/n8n-backup.service:
[Unit]
Description=Automated PostgreSQL Backup for n8n Engine
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
User=root
WorkingDirectory=/opt/automation-stack
ExecStart=/usr/bin/python3 /opt/automation-stack/db_backup.py
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Create the corresponding systemd timer unit file at /etc/systemd/system/n8n-backup.timer:
[Unit]
Description=Run n8n PostgreSQL Backup Daily at 02:00 UTC
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
Reload the systemd manager configuration, enable the timer, and verify its target schedule execution:
sudo systemctl daemon-reload
sudo systemctl enable --now n8n-backup.timer
sudo systemctl list-timers --all | grep n8n-backup
You can run an immediate dry run execution of the backup unit to confirm system functionality:
sudo systemctl start n8n-backup.service
sudo journalctl -u n8n-backup.service -n 20 --no-pager
This stack gives you high-performance, self-hosted workflow execution on Hetzner VPS. It includes database isolation, HTTPS proxying via Caddy, automated database health management, and systemd backup tasks.
Getting Started
To implement this workflow infrastructure, acquire your base dependencies:
- Reserve a high-performance cloud instance on Hetzner VPS or look into Contabo VPS / DigitalOcean.
- Register your custom production domain with Namecheap.
- Deploy using open-source engines or start with n8n Cloud before migrating to self-hosted servers.
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-DEVTO for priority. Or DM at chasebot.online.
Originally published on Automation Insider.
Top comments (0)