Skip to content

How to Self-Host n8n for AI Automation Workflows

Problem

I wanted to build AI automation workflows without paying expensive cloud subscription fees. n8n cloud costs $20-600/month depending on workflow volume. That’s too much for my small business experiments.

I tried Zapier first. Then Make.com. Both charged per execution and quickly became expensive. I needed something I could run on my own server.

Reddit users recommended n8n self-hosted:

“Running it self-hosted for about a year, connects to pretty much everything via HTTP nodes and the agent stuff got way better since 1.x.”

“Not as polished as some of these but I’m not sure any of them give you the same level of control.”

The key insight: n8n self-hosted costs $5-20/month server resources vs $20-600/month for cloud platforms.

Environment

  • Ubuntu 22.04 VPS (or any Linux server)
  • Docker and Docker Compose installed
  • PostgreSQL 15 for data storage
  • Caddy or Nginx for reverse proxy (optional)
  • OpenAI API key or Anthropic API key for AI nodes
  • Ollama for local LLMs (optional)

Solution

I set up n8n self-hosted in three steps: basic Docker setup, production configuration, and AI workflow creation.

Basic Docker Setup

I started with a simple Docker Compose file:

docker-compose.yml
version: "3.8"
services:
postgres:
image: postgres:15
restart: always
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: n8n_password
POSTGRES_DB: n8n
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
interval: 10s
timeout: 5s
retries: 5
n8n:
image: n8nio/n8n:latest
restart: always
ports:
- "5678:5678"
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: 5432
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: n8n_password
N8N_ENCRYPTION_KEY: my-super-secret-encryption-key-change-this
N8N_HOST: localhost
N8N_PORT: 5678
N8N_PROTOCOL: http
GENERIC_TIMEZONE: UTC
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:
n8n_data:

Then I ran the setup:

Terminal
# Create project directory
mkdir ~/n8n-setup && cd ~/n8n-setup
# Create docker-compose.yml
# (paste the file above)
# Start n8n
docker-compose up -d
# Check logs
docker-compose logs -f n8n
Terminal Output
n8n | Initializing n8n process...
n8n | Connecting to PostgreSQL database...
n8n | n8n started on port 5678
n8n | Editor is now accessible via: http://localhost:5678

I opened http://localhost:5678 in my browser and saw the n8n dashboard.

Common Mistake: Using SQLite

I initially skipped PostgreSQL and used SQLite. This caused performance issues when I had more than 50 workflows:

Error Output
Error: SQLITE_BUSY: database is locked
Workflow execution failed due to concurrent access

Switching to PostgreSQL fixed this. PostgreSQL handles concurrent connections properly.

Production Setup with SSL

For production, I added Caddy as a reverse proxy with automatic SSL:

docker-compose.prod.yml
version: "3.8"
services:
postgres:
image: postgres:15
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: n8n
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d n8n"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: always
volumes:
- redis_data:/data
n8n:
image: n8nio/n8n:latest
restart: always
environment:
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}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
N8N_HOST: ${N8N_DOMAIN}
N8N_PORT: 5678
N8N_PROTOCOL: https
WEBHOOK_URL: https://${N8N_DOMAIN}/
GENERIC_TIMEZONE: ${TIMEZONE}
EXECUTIONS_MODE: queue
QUEUE_BULL_REDIS_HOST: redis
QUEUE_BULL_REDIS_PORT: 6379
QUEUE_BULL_REDIS_DB: 0
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
caddy:
image: caddy:latest
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- caddy_data:/data
- caddy_config:/config
- ./Caddyfile:/etc/caddy/Caddyfile
depends_on:
- n8n
volumes:
postgres_data:
redis_data:
n8n_data:
caddy_data:
caddy_config:

I created the environment file:

.env
POSTGRES_USER=n8n_prod
POSTGRES_PASSWORD=your-strong-password-here-min-32-characters
N8N_ENCRYPTION_KEY=your-encryption-key-min-32-characters-random-string
N8N_DOMAIN=n8n.yourdomain.com
TIMEZONE=America/New_York

And the Caddyfile:

Caddyfile
n8n.yourdomain.com {
reverse_proxy n8n:5678
encode gzip
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}

Then I deployed to production:

Terminal
# Copy files to server
scp docker-compose.prod.yml .env Caddyfile user@your-server:/opt/n8n/
# SSH to server
ssh user@your-server
# Start production stack
cd /opt/n8n
docker-compose -f docker-compose.prod.yml up -d
# Check status
docker-compose -f docker-compose.prod.yml ps
Terminal Output
NAME STATUS PORTS
n8n-postgres running (healthy) 5432/tcp
n8n-redis running 6379/tcp
n8n running 5678/tcp
n8n-caddy running 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp

I accessed https://n8n.yourdomain.com with automatic SSL from Caddy.

Security Configuration

I set critical environment variables for security:

Terminal
# Generate strong encryption key
openssl rand -base64 32
# Generate strong PostgreSQL password
openssl rand -base64 32

The N8N_ENCRYPTION_KEY encrypts all credentials stored in n8n. Without this, anyone with database access could read your API keys.

AI Workflow with OpenAI Integration

Now I built an AI automation workflow. I created a workflow that monitors a Gmail inbox and summarizes emails using AI:

email-summary-workflow.json
{
"name": "AI Email Summary",
"nodes": [
{
"parameters": {
"pollTimes": {
"item": [
{
"mode": "everyHour"
}
]
}
},
"id": "schedule-trigger",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.1,
"position": [250, 300]
},
{
"parameters": {
"resource": "message",
"operation": "getAll",
"limit": 10,
"filters": {
"q": "is:unread"
}
},
"id": "gmail-node",
"name": "Get Unread Emails",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2,
"position": [450, 300],
"credentials": {
"gmail": {
"id": "gmail-credential-id",
"name": "Gmail account"
}
}
},
{
"parameters": {
"resource": "chat",
"model": "gpt-4o-mini",
"messages": {
"values": [
{
"role": "system",
"content": "You are an email summarizer. Create a brief, actionable summary of each email."
},
{
"role": "user",
"content": "=Summarize this email:\nSubject: {{ $json.subject }}\nFrom: {{ $json.from }}\nBody: {{ $json.body }}"
}
]
}
},
"id": "openai-node",
"name": "Summarize with AI",
"type": "@n8n/n8n-nodes-langchain.openAi",
"typeVersion": 1.3,
"position": [650, 300],
"credentials": {
"openAiApi": {
"id": "openai-credential-id",
"name": "OpenAI account"
}
}
},
{
"parameters": {
"resource": "message",
"channel": "#email-summary",
"text": "=**New Email Summary**\n\nFrom: {{ $('Get Unread Emails').item.json.from }}\nSubject: {{ $('Get Unread Emails').item.json.subject }}\n\nSummary: {{ $json.message.content }}\n\n---\nProcessed at: {{ $now }}"
},
"id": "slack-node",
"name": "Send to Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2,
"position": [850, 300],
"credentials": {
"slackApi": {
"id": "slack-credential-id",
"name": "Slack account"
}
}
}
],
"connections": {
"Schedule Trigger": {
"main": [
[
{
"node": "Get Unread Emails",
"type": "main",
"index": 0
}
]
]
},
"Get Unread Emails": {
"main": [
[
{
"node": "Summarize with AI",
"type": "main",
"index": 0
}
]
]
},
"Summarize with AI": {
"main": [
[
{
"node": "Send to Slack",
"type": "main",
"index": 0
}
]
]
}
}
}

AI Agent Node with Local LLMs

I also tried connecting to local LLMs via Ollama for cost savings:

docker-compose.yml (adding Ollama)
# Add this service to your docker-compose.yml
ollama:
image: ollama/ollama:latest
restart: always
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]

Then I pulled a model:

Terminal
# Pull a model (on server)
docker exec -it ollama ollama pull llama3.2
# Or pull a coding-focused model
docker exec -it ollama ollama pull codellama:7b

I created an AI Agent workflow using Ollama:

ollama-agent-workflow.json
{
"name": "AI Agent with Local LLM",
"nodes": [
{
"parameters": {},
"id": "manual-trigger",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [250, 300]
},
{
"parameters": {
"agent": "conversationalAgent",
"prompt": "You are a helpful assistant that can analyze data and answer questions.",
"hasOutputParser": false,
"model": {
"modelId": "llama3.2",
"baseUrl": "http://ollama:11434"
}
},
"id": "ai-agent",
"name": "AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 1.4,
"position": [450, 300]
},
{
"parameters": {
"values": {
"string": [
{
"name": "query",
"value": "Analyze the attached data and provide insights"
}
]
}
},
"id": "set-input",
"name": "Set Input",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [650, 300]
}
],
"connections": {
"Manual Trigger": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"AI Agent": {
"main": [
[
{
"node": "Set Input",
"type": "main",
"index": 0
}
]
]
}
}
}

Backup Configuration

I set up automatic backups to prevent data loss:

backup.sh
#!/bin/bash
# n8n backup script
BACKUP_DIR="/opt/n8n-backups"
DATE=$(date +%Y%m%d_%H%M%S)
# Create backup directory
mkdir -p $BACKUP_DIR
# Backup PostgreSQL database
docker exec n8n-postgres pg_dump -U n8n n8n > $BACKUP_DIR/n8n_db_$DATE.sql
# 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 7 days of backups
find $BACKUP_DIR -type f -mtime +7 -delete
echo "Backup completed: $DATE"

I set up a cron job:

Terminal
# Add to crontab
crontab -e
# Add this line for daily backup at 2 AM
0 2 * * * /opt/n8n/backup.sh >> /opt/n8n/backup.log 2>&1

Cost Comparison

I compared the costs:

OptionMonthly CostWorkflow LimitData Privacy
n8n Cloud Starter$205,000 executionsCloud stored
n8n Cloud Pro$5050,000 executionsCloud stored
n8n Cloud Enterprise$600+UnlimitedCloud stored
Zapier Starter$19.99750 tasksCloud stored
Make Free$01,000 operationsCloud stored
Make Core$1010,000 operationsCloud stored
n8n Self-hosted$5-20UnlimitedYour server

My self-hosted n8n on a $10/month VPS handles unlimited workflows with full data privacy.

Why Self-Hosting Matters

I found four key benefits:

  1. Cost Savings: $5-20/month server cost vs $20-600/month cloud
  2. Data Privacy: All credentials and workflows stay on my infrastructure
  3. AI Integration Flexibility: I connect to OpenAI, Anthropic, or local LLMs
  4. No Vendor Lock-in: I export workflows as JSON files and move anywhere

Common Mistakes to Avoid

I made these mistakes and learned from them:

  1. Skipping PostgreSQL: Use SQLite by default, limits performance with many workflows
  2. Exposing without SSL: Never run n8n on HTTP in production
  3. Ignoring encryption key: Without N8N_ENCRYPTION_KEY, credentials are not encrypted
  4. No backups: Database corruption means losing all workflows

Summary

In this post, I showed how to set up n8n self-hosted for AI automation workflows. The key point is using Docker Compose with PostgreSQL for a production-ready setup that costs $5-20/month instead of $20-600/month for cloud alternatives.

I covered basic Docker setup, production configuration with Caddy SSL, AI workflow creation with OpenAI and local LLMs via Ollama, backup configuration, and cost comparison. The main benefits: cost savings, data privacy, AI flexibility, and no vendor lock-in.

Start with the basic setup, add PostgreSQL and SSL for production, then build your AI workflows.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments