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:
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:
# Create project directorymkdir ~/n8n-setup && cd ~/n8n-setup
# Create docker-compose.yml# (paste the file above)
# Start n8ndocker-compose up -d
# Check logsdocker-compose logs -f n8nn8n | Initializing n8n process...n8n | Connecting to PostgreSQL database...n8n | n8n started on port 5678n8n | Editor is now accessible via: http://localhost:5678I 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: SQLITE_BUSY: database is lockedWorkflow execution failed due to concurrent accessSwitching 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:
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:
POSTGRES_USER=n8n_prodPOSTGRES_PASSWORD=your-strong-password-here-min-32-charactersN8N_ENCRYPTION_KEY=your-encryption-key-min-32-characters-random-stringN8N_DOMAIN=n8n.yourdomain.comTIMEZONE=America/New_YorkAnd the 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:
# Copy files to serverscp docker-compose.prod.yml .env Caddyfile user@your-server:/opt/n8n/
# SSH to serverssh user@your-server
# Start production stackcd /opt/n8ndocker-compose -f docker-compose.prod.yml up -d
# Check statusdocker-compose -f docker-compose.prod.yml psNAME STATUS PORTSn8n-postgres running (healthy) 5432/tcpn8n-redis running 6379/tcpn8n running 5678/tcpn8n-caddy running 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcpI accessed https://n8n.yourdomain.com with automatic SSL from Caddy.
Security Configuration
I set critical environment variables for security:
# Generate strong encryption keyopenssl rand -base64 32
# Generate strong PostgreSQL passwordopenssl rand -base64 32The 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:
{ "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:
# 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:
# Pull a model (on server)docker exec -it ollama ollama pull llama3.2
# Or pull a coding-focused modeldocker exec -it ollama ollama pull codellama:7bI created an AI Agent workflow using Ollama:
{ "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:
#!/bin/bash
# n8n backup scriptBACKUP_DIR="/opt/n8n-backups"DATE=$(date +%Y%m%d_%H%M%S)
# Create backup directorymkdir -p $BACKUP_DIR
# Backup PostgreSQL databasedocker exec n8n-postgres pg_dump -U n8n n8n > $BACKUP_DIR/n8n_db_$DATE.sql
# Backup n8n data volumedocker 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 backupsfind $BACKUP_DIR -type f -mtime +7 -delete
echo "Backup completed: $DATE"I set up a cron job:
# Add to crontabcrontab -e
# Add this line for daily backup at 2 AM0 2 * * * /opt/n8n/backup.sh >> /opt/n8n/backup.log 2>&1Cost Comparison
I compared the costs:
| Option | Monthly Cost | Workflow Limit | Data Privacy |
|---|---|---|---|
| n8n Cloud Starter | $20 | 5,000 executions | Cloud stored |
| n8n Cloud Pro | $50 | 50,000 executions | Cloud stored |
| n8n Cloud Enterprise | $600+ | Unlimited | Cloud stored |
| Zapier Starter | $19.99 | 750 tasks | Cloud stored |
| Make Free | $0 | 1,000 operations | Cloud stored |
| Make Core | $10 | 10,000 operations | Cloud stored |
| n8n Self-hosted | $5-20 | Unlimited | Your 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:
- Cost Savings: $5-20/month server cost vs $20-600/month cloud
- Data Privacy: All credentials and workflows stay on my infrastructure
- AI Integration Flexibility: I connect to OpenAI, Anthropic, or local LLMs
- 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:
- Skipping PostgreSQL: Use SQLite by default, limits performance with many workflows
- Exposing without SSL: Never run n8n on HTTP in production
- Ignoring encryption key: Without
N8N_ENCRYPTION_KEY, credentials are not encrypted - 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:
- 👨💻 n8n Documentation
- 👨💻 n8n Docker Guide
- 👨💻 Reddit: Self-hosted automation tools
- 👨💻 Ollama Local LLMs
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments