n8n is one of the fastest ways to automate internal operations, but many teams run it with default settings and discover reliability problems only after critical workflows fail. This guide shows a production-first deployment of n8n on Ubuntu using Docker Compose, Nginx, and PostgreSQL. The pattern is practical for startups and mid-size teams that need stable webhook processing, predictable restart behavior, and a clear path for backups and recovery. We use Nginx for TLS termination and request controls, PostgreSQL for persistent workflow state, and Docker Compose for repeatable operations. The objective is not just to get containers running, but to produce an environment your team can monitor, patch, and restore without guesswork.
Architecture and flow overview
The deployment contains three core services: n8n for workflow execution, PostgreSQL for state and execution metadata, and Nginx as the public edge. External clients and SaaS callbacks only touch Nginx on 443; Nginx forwards trusted traffic to the n8n container on the private Docker network. n8n connects to PostgreSQL using an internal service hostname, keeping the database off the public interface. This separation reduces accidental exposure and makes policy changes easy: rate limiting, request-size caps, and TLS renewal happen at the edge, while application and data tiers remain isolated.
# Request flow
# Browser / webhook provider -> Nginx :443 -> n8n :5678
# n8n -> PostgreSQL :5432 (internal Docker network only)
# Backup job -> PostgreSQL dump + n8n encrypted config volume
If the copy button does not work in your browser/editor, manually select and copy the command block.
Prerequisites
- Ubuntu 22.04+ host with at least 2 vCPU, 4 GB RAM (8 GB recommended for bursty workflow loads).
- A domain name pointing to the server public IP, e.g.,
automation.example.com. - Docker Engine and Docker Compose plugin installed.
- Ports 80 and 443 open to the internet.
- A non-root sudo user for operations.
- Mail/SMTP credentials if you plan to enable user invites and alerts.
sudo apt update
sudo apt install -y ca-certificates curl gnupg
# Install Docker from official repository if not already installed
docker --version
docker compose version
If the copy button does not work in your browser/editor, manually select and copy the command block.
Step-by-step deployment
1) Create project directories and environment file
Use a single project root so operational tasks are predictable. Keep secrets in a dedicated .env file and avoid hardcoding values in Compose manifests. Set a strong N8N_ENCRYPTION_KEY; this key protects credentials stored by n8n and is mandatory for reliable restore behavior.
sudo mkdir -p /opt/n8n/{nginx,postgres,data,backups}
cd /opt/n8n
openssl rand -hex 32 # use as N8N_ENCRYPTION_KEY
cat > .env <<'EOF'
DOMAIN=automation.example.com
POSTGRES_DB=n8n
POSTGRES_USER=n8n
POSTGRES_PASSWORD=CHANGE_ME_STRONG_PASSWORD
N8N_ENCRYPTION_KEY=CHANGE_ME_64_HEX
N8N_BASIC_AUTH_USER=opsadmin
N8N_BASIC_AUTH_PASSWORD=CHANGE_ME_LONG_PASSWORD
GENERIC_TIMEZONE=UTC
EOF
If the copy button does not work in your browser/editor, manually select and copy the command block.
2) Create Docker Compose stack
This Compose file pins service roles clearly: PostgreSQL gets a dedicated volume and health check; n8n waits for database readiness and runs with explicit environment values. Restart policies are set to unless-stopped for predictable service survival after host reboots.
cat > docker-compose.yml <<'EOF'
services:
postgres:
image: postgres:16
container_name: n8n-postgres
restart: unless-stopped
env_file: .env
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- ./postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
n8n:
image: n8nio/n8n:latest
container_name: n8n-app
restart: unless-stopped
env_file: .env
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- N8N_HOST=${DOMAIN}
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://${DOMAIN}/
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER}
- N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD}
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
ports:
- "127.0.0.1:5678:5678"
volumes:
- ./data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
EOF
If the copy button does not work in your browser/editor, manually select and copy the command block.
3) Configure Nginx reverse proxy
Nginx should be the only publicly reachable component. We bind n8n to localhost and forward through Nginx with hardened defaults: preserved client headers, reasonable body limit for webhook payloads, and conservative read timeouts for longer executions.
sudo apt install -y nginx certbot python3-certbot-nginx
sudo tee /etc/nginx/sites-available/n8n >/dev/null <<'EOF'
server {
listen 80;
server_name automation.example.com;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
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 300;
client_max_body_size 20m;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/n8n
sudo nginx -t && sudo systemctl reload nginx
If the copy button does not work in your browser/editor, manually select and copy the command block.
4) Enable TLS certificates
Issue Let's Encrypt certificates and let Certbot manage renewal. After issuance, test auto-renewal once to ensure certificates continue rotating without manual intervention.
sudo certbot --nginx -d automation.example.com --redirect --agree-tos -m [email protected] --non-interactive
sudo certbot renew --dry-run
If the copy button does not work in your browser/editor, manually select and copy the command block.
5) Launch and initialize services
Start the stack and confirm both application and database are healthy. Initial startup can take longer while n8n prepares state and migrations.
cd /opt/n8n
docker compose pull
docker compose up -d
docker compose ps
docker logs --tail=100 n8n-app
If the copy button does not work in your browser/editor, manually select and copy the command block.
6) Create backup automation
At minimum, back up PostgreSQL data and the n8n data directory. Store rotation policy with your script so retention behavior is deterministic. A simple daily cron works for many teams; for stricter RPO targets, move to hourly snapshots.
cat > /opt/n8n/backup.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%F-%H%M%S)
mkdir -p /opt/n8n/backups
docker exec n8n-postgres pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > "/opt/n8n/backups/n8n-db-$STAMP.sql"
tar -czf "/opt/n8n/backups/n8n-data-$STAMP.tar.gz" -C /opt/n8n data
find /opt/n8n/backups -type f -mtime +14 -delete
EOF
chmod +x /opt/n8n/backup.sh
( crontab -l 2>/dev/null; echo "15 2 * * * /opt/n8n/backup.sh" ) | crontab -
If the copy button does not work in your browser/editor, manually select and copy the command block.
7) Harden service exposure and secrets handling
Keep database and n8n internal ports private. Never commit .env to source control. Restrict file permissions so only your deployment user can read secrets, and rotate any leaked values immediately. If you integrate cloud services, prefer platform-managed secret stores and inject values at runtime.
chmod 600 /opt/n8n/.env
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw deny 5678/tcp
sudo ufw deny 5432/tcp
sudo ufw enable
If the copy button does not work in your browser/editor, manually select and copy the command block.
8) Define upgrade routine
For stable operations, document upgrade sequencing: backup first, pull image updates, apply containers, then run smoke tests. This prevents silent breakage after unattended updates and gives your team a known rollback point.
cd /opt/n8n
/opt/n8n/backup.sh
docker compose pull
docker compose up -d
docker compose ps
# Smoke test one inbound webhook and one scheduled workflow after each upgrade
If the copy button does not work in your browser/editor, manually select and copy the command block.
Configuration and secrets handling best practices
Production n8n environments should treat credentials as first-class assets. Use a unique encryption key per environment, avoid password reuse between application auth and database auth, and separate operator credentials from automation accounts. If multiple engineers manage the host, move secrets out of flat files into a managed vault and grant least-privilege access. For webhook-heavy setups, enforce explicit queue/retry design inside workflows so temporary API failures do not cascade into duplicate side effects. Finally, define ownership: who rotates secrets, who validates backup restores, and who approves workflow changes that touch external billing or identity systems.
Verification checklist
- n8n login loads over HTTPS with valid certificate chain.
- Database health check returns healthy and restart does not lose workflows.
- Webhook endpoint accepts test payload and triggers expected execution path.
- Backup script produces both SQL and data archive files.
- Restore drill in a staging host confirms credentials and workflow integrity.
curl -I https://automation.example.com
docker inspect --format="{{json .State.Health}}" n8n-postgres
ls -lh /opt/n8n/backups | tail -n 5
If the copy button does not work in your browser/editor, manually select and copy the command block.
Common issues and fixes
n8n UI loads but webhooks fail with 404 or invalid URL
This usually means WEBHOOK_URL, N8N_HOST, or proxy headers are misaligned. Confirm Nginx passes X-Forwarded-Proto and that n8n uses HTTPS in environment settings.
Database authentication errors after password rotation
Update both .env and PostgreSQL credentials consistently, then restart services. If the old password remains cached in one service definition, n8n will loop on reconnect.
Long workflows timing out behind proxy
Increase Nginx proxy_read_timeout and evaluate whether long-running logic should be split into asynchronous steps. Large monolithic workflows are harder to debug and recover.
Credentials missing after restore
Restores require the original N8N_ENCRYPTION_KEY. Keep key escrow in your secret manager and test restore procedures quarterly, not only during incidents.
FAQ
Should I use SQLite for n8n in production?
For small lab environments SQLite can work, but production automation should use PostgreSQL. You get stronger concurrency handling, better durability options, and cleaner backup/restore workflows when execution volume grows.
Can I run n8n without basic auth if I already have TLS?
No. TLS protects transport, not access control. Keep n8n basic auth or SSO in front of the UI, and additionally restrict IP access for admin endpoints where possible.
How often should backups run?
Daily is the minimum baseline. If workflows trigger financial, customer, or compliance actions, move to hourly database dumps plus frequent filesystem snapshots to reduce potential data loss windows.
How do I safely upgrade n8n versions?
Always back up first, pull images in staging, run smoke tests on representative workflows, then promote to production. Avoid blind latest-tag upgrades on business-critical automation hosts.
What is the quickest way to validate webhook reliability?
Use a synthetic test sender that posts a known payload every few minutes, then alert if no matching execution appears in n8n. This catches DNS, TLS, and routing failures early.
Can I scale this architecture later?
Yes. Start with a single host, then move PostgreSQL to managed HA and place n8n workers behind queue mode as throughput increases. Keep secrets and observability practices consistent during the transition.
Related guides
- Production Guide Deploy Openobserve With Docker Compose Traefik Clickhouse On Ubuntu 400
- Production Guide Deploy Docmost With Docker Compose Traefik And Postgresql On Ubuntu 396
- Production Guide Deploy Grafana Loki With Docker Compose Traefik S3 On Ubuntu 395
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.