# n8n Self-Hosted 5 Real Production Pitfalls: SQLite/SSL/Permissions/Backup/Timeout
This article contains affiliate links (n8n itself is free and open-source; Docker/PostgreSQL tools mentioned may have affiliate programs).
Self-hosting n8n for workflow automation sounds straightforward: one Docker command, configure a few nodes, and your automation is running. But when you actually push it to production, 5 "looks fine but explodes on deployment" problems show up one by one: SQLite data loss, SSL certificate issues, container running as root, backups that were never tested, and execution timeouts silently killing workflows.
This is a real case review of those 5 pitfalls, each with specific error messages, the troubleshooting process, and reproducible fixes.
n8n Production Architecture Overview
Before diving into pitfalls, here's the minimum production architecture:
User → Nginx(443) → n8n(:5678) → PostgreSQL(:5432)
↓
n8n_files/ ← attachments/binary storage
- **n8n version**: Use `n8nio/n8n:1.65.0` or later (latest as of 2026-06)
- **Database**: PostgreSQL 16+ for production (SQLite is not acceptable)
- **Reverse proxy**: Nginx or Caddy, **never expose port 5678 directly**
- **RAM**: 4GB minimum, PostgreSQL takes ~512MB, n8n takes ~1GB
Quick start with Docker Compose:
version: '3.8'
services:
n8n:
image: n8nio/n8n:1.65.0
restart: always
ports:
- "127.0.0.1:5678:5678"
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n_user
- DB_POSTGRESDB_PASSWORD=${N8N_DB_PASSWORD}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- WEBHOOK_URL=https://your-domain.com/
- EXECUTIONS_MODE=regular
- EXECUTIONS_TIMEOUT=600
- GENERIC_TIMEZONE=Asia/Shanghai
volumes:
- n8n_files:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16-alpine
restart: always
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=n8n_user
- POSTGRES_PASSWORD=${N8N_DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n_user -d n8n"]
interval: 10s
timeout: 5s
retries: 5
volumes:
n8n_files:
postgres_data:
.env file:
N8N_DB_PASSWORD=your_secure_random_password_32chars
N8N_ENCRYPTION_KEY=another_32_char_random_key_for_encryption
---
💣 Pitfall 1: SQLite in Production Causes Data Loss
Specific Error
ERROR: Database lock timeout
SQLITE_BUSY: database is locked
Or execution history "disappearing" — after redeployment, all historical records are gone.
Root Cause
n8n uses SQLite by default, which is fine for development/testing but has serious production risks:
- SQLite uses file locking; **concurrent writes will lock it immediately** (especially bad in multi-node setups)
- Docker volumes get wiped when you run `docker compose down -v`
- No WAL mode — power loss can corrupt the database
- Barely usable in single-instance; workflow saves randomly fail with multiple users
Debugging Commands
# Check n8n database file size (SQLite mode)
ls -lh /home/node/.n8n/database.sqlite
# Check if running inside Docker (temporary fix)
docker exec -it ls -la /home/node/.n8n/
Solution
Migrate to PostgreSQL immediately:
# 1. Export data from old container (if available)
docker exec -it n8n_old n8n export:workflows --backupFile /tmp/workflows.json
# 2. Rebuild with PostgreSQL docker-compose
# (see docker-compose.yaml example above)
# 3. Import workflows after startup
docker exec -i n8n n8n import:workflows --input /tmp/workflows.json
Configure PostgreSQL connection (docker-compose environment variables):
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres # Docker Compose service name
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n_user
DB_POSTGRESDB_PASSWORD=xxx
Verify database type:
Go to n8n Settings → Source Control, confirm it shows PostgreSQL 16.x not SQLite 3.x.
Prevention
- Use PostgreSQL from day one — don't wait for data loss to migrate
- Never run `down -v` in production (the `-v` flag deletes named volumes)
---
💣 Pitfall 2: Nginx Reverse Proxy Causes Webhook 502/404
Specific Error
# Browser shows n8n homepage fine, but webhook calls fail:
502 Bad Gateway
# or
404 Not Found
Manual trigger shows nothing in the webhook logs.
Root Cause
Nginx location block is misconfigured, or WEBHOOK_URL environment variable is wrong. n8n's webhook path uses /webhook/ prefix — Nginx must pass this through correctly:
# ❌ Wrong configuration (misses /webhook/ path)
location / {
proxy_pass http://127.0.0.1:5678;
}
# ✅ Correct configuration
location / {
proxy_pass http://127.0.0.1:5678;
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;
# WebSocket support (n8n editor live preview needs this)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Critical: increase timeout
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
# Webhook-dedicated path (if using separate domain)
location /webhook/ {
proxy_pass http://127.0.0.1:5678;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_read_timeout 300s;
}
Debugging Steps
# 1. Confirm n8n process is healthy inside container
docker exec -it n8n curl -s http://localhost:5678 | head -20
# 2. Confirm WEBHOOK_URL is a publicly accessible full URL
docker exec -it n8n env | grep WEBHOOK
# Correct format: https://n8n.your-domain.com/
# 3. Test webhook reachability from public internet
curl -X POST https://n8n.your-domain.com/webhook/test/your-webhook-id -v
Solution
Add to n8n service in docker-compose.yaml:
environment:
- WEBHOOK_URL=https://n8n.your-domain.com/
# NOT http://localhost:5678 — that's the internal container address
Then reload Nginx:
nginx -t && nginx -s reload
Prevention
- After setting `WEBHOOK_URL`, verify public reachability via n8n Workflow Settings → Test Webhook
- Never expose n8n port 5678 directly to the public internet
---
💣 Pitfall 3: Execution Timeout Silently Kills Workflows
Specific Error
Execution timed out after 300 seconds
Workflow "XXX" was running for longer than the configured timeout of 300 seconds.
The workflow logic is correct but it just stops halfway through, and the error log only shows "timed out."
Root Cause
n8n's default execution timeout is 300 seconds (5 minutes), which is far too short for workflows that call external APIs or process bulk data. But this timeout setting is buried deep in the configuration — many people don't even know it exists.
Debugging Commands
# Check current timeout configuration
docker exec -it n8n env | grep EXECUTIONS_TIMEOUT
# Output may be empty (default 300)
# Check recent timeout logs
docker logs n8n --since 10m | grep -i timeout
Solution
**Option 1: Change global default timeout** (docker-compose.yaml):
environment:
- EXECUTIONS_TIMEOUT=600 # 10 minutes (600 seconds)
# Or set to 0 for unlimited (use with caution)
Option 2: Change individual workflow timeout (n8n UI):
1. Open workflow → Click Workflow Settings (top right)
2. Find Execution Timeout
3. Set to 600 (seconds) or check **Allow manual executions to run longer**
Option 3: Per-node API timeout (most commonly overlooked):
Each HTTP Request node has its own timeout (default 300 seconds), configured in node settings:
Options → Timeout (ms): 600000 ← 10 minutes
Prevention
- Always set explicit timeout in Workflow Settings before going live
- For bulk data workflows, use **Split In Batches** node to process in batches — avoid large single executions
- Set up alerts: notify when execution duration exceeds 5 minutes
// Add an Error Trigger at the end of a workflow
// Sends email notification when timeout occurs
---
💣 Pitfall 4: Backups Were Never Tested — Data Loss Discovered Too Late
Specific Error
The most painful category — after server migration or reinstallation, when trying to restore n8n data:
# Backup file won't open
Error: invalid tar header
# Backup file is empty
ls -lh n8n_backup.tar.gz
# -rw-r--r-- 1 root root 0 Jan 1 00:00 n8n_backup.tar.gz
# PostgreSQL backup missing credentials
# Credentials are stored encrypted separately — exporting workflows alone is not enough
Root Cause
Three common backup mistakes:
1. Only backing up workflows, not credentials: n8n credentials (API keys, database passwords, etc.) are stored encrypted separately in PostgreSQL — exporting workflows is not enough
2. **Backup script has no execute permission**: forgot chmod +x backup.sh
3. Backup goes to container instead of mounted volume: deleted when container is removed
Correct Backup Solution
Complete backup script (workflows + credentials + PostgreSQL):
#!/bin/bash
# n8n_backup.sh — run on the host machine, not inside container
BACKUP_DIR="/opt/n8n_backups"
DATE=$(date +%Y%m%d_%H%M%S)
CONTAINER_NAME="n8n"
mkdir -p $BACKUP_DIR
# 1. Export all workflows (JSON format)
docker exec $CONTAINER_NAME n8n export:workflows --output /tmp/workflows_$DATE.json
# 2. Export all credentials (encrypted, needed for migration)
docker exec $CONTAINER_NAME n8n export:credentials --output /tmp/credentials_$DATE.json
# 3. PostgreSQL full backup
docker exec postgres pg_dump -U n8n_user n8n > $BACKUP_DIR/n8n_db_$DATE.sql
# 4. Package everything
tar czf $BACKUP_DIR/n8n_full_backup_$DATE.tar.gz \
/tmp/workflows_$DATE.json \
/tmp/credentials_$DATE.json \
$BACKUP_DIR/n8n_db_$DATE.sql
# 5. Clean up backups older than 7 days
find $BACKUP_DIR -name "n8n_full_backup_*.tar.gz" -mtime +7 -delete
echo "Backup completed: n8n_full_backup_$DATE.tar.gz"
**Set up cron job** (crontab -e):
0 3 * * * /opt/n8n_backup.sh >> /var/log/n8n_backup.log 2>&1
Verify backup validity (critical — don't skip this):
# 1. Verify tar package integrity
tar tzf n8n_full_backup_20260615_030000.tar.gz
# 2. Verify workflow JSON is readable
cat /tmp/workflows_20260615_030000.json | python3 -m json.tool > /dev/null && echo "JSON OK"
# 3. Verify SQL backup
grep -c "COPY" n8n_db_20260615_030000.sql # Should be > 0
Prevention
- **Do a full restore drill in a test environment every quarter** — verify the backup actually works
- Don't store backup files on the same server — use `rclone` to sync to S3/OSS/MinIO
---
💣 Pitfall 5: No Error Notifications — Silent Workflow Failures
Specific Error
This category has no clear error message, but causes the biggest business impact — workflows run daily but some nodes quietly fail, n8n status looks green, and you assume everything is fine.
Root Cause
n8n does not send execution result notifications by default. Many workflows run in "silent failure" mode:
- An API call returns non-200 but doesn't route to Error Trigger
- A condition node routes data to an ignored branch
- Manual workflow sits forgotten in the queue
Debugging Method
Check Execution History in n8n:
1. Settings → Source Control → Executions
2. Filter by Status: Error
3. Find the failing workflow, check which node failed
# Check last 24h execution records (via API)
curl -s -u admin:${N8N_API_KEY} \
"https://n8n.your-domain.com/rest/executions?limit=50&filter={}" \
| jq '.data[] | {id, workflowId, status, finished, startTime}'
Solution
Add Error Trigger to every workflow (built-in n8n node):
1. Open workflow → Click + node selector
2. Search for Error Trigger → Add it
3. Connect Error Trigger to Send Email or Slack Message node
Example configuration:
[Any Node] → (on error) → Error Trigger → Slack Message
↓
Email Notification
Subject: Workflow {workflow.name} failed on {date}
Node: {node.name}
Error: {error.message}
**Configure global error alerts** (in docker-compose.yaml):
environment:
# Email notification on workflow execution failure
- N8N_EMAIL_MODE=smtp
- N8N_SMTP_HOST=smtp.example.com
- N8N_SMTP_PORT=587
- N8N_SMTP_USER=noreply@example.com
- N8N_SMTP_PASS=xxx
- N8N_SMTP_FROM=n8n@example.com
Prevention
- Error Trigger (or at minimum Email notification) is mandatory before any workflow goes live
- Check Execution History weekly — look for "looks fine but has failures" patterns
- Use n8n's **Prometheus metrics endpoint** (`/metrics`) with Grafana monitoring:
environment:
- N8N_METRICS=true
---
Complete Production Checklist
Save this checklist and review it before every deployment:
□ PostgreSQL instead of SQLite
□ N8N_ENCRYPTION_KEY is a unique 32-char random key (never reuse)
□ WEBHOOK_URL is set to public HTTPS URL
□ Nginx proxy passes /webhook/ path and WebSocket support is enabled
□ EXECUTIONS_TIMEOUT=600 or higher (adjust as needed)
□ Backup script is configured and recovery has been tested
□ Error Trigger notifications are configured
□ N8N_METRICS=true is enabled and connected to monitoring
□ SSL certificate auto-renews via Let's Encrypt / Certbot
□ Database and n8n_files use named volumes, not bind mounts
---
Next Steps
Once your self-hosted n8n is stable, you can expand further:
- **n8n + Langfuse: Monitor Workflow Costs**: Langfuse tracks Token consumption and latency for every LLM call
- **n8n + Claude Code MCP Automation**: Trigger workflow orchestration with natural language
- **Self-Hosted AI Workflow Complete Guide**: Docker Compose one-command setup for Ollama + Qdrant + n8n
---
👉 Want low-cost AI workflow access? MiniMax Token Plan provides stable API calls, works great with n8n and AI toolchains:
https://platform.minimaxi.com/subscribe/token-plan?code=E5yur9NOub&source=link
📌 This article was AI-assisted generated and human-reviewed | TechPassive — An AI-driven content testing site focused on real tool reviews
🔗 Recommended Tools
These are carefully selected tools. Using our affiliate links supports us to keep producing quality content: