Ep 03: Environment Setup — Docker Compose Deployment & Security Configuration
Deployment Architecture Overview
Before getting hands-on, understand the full system we're building:
graph TB
subgraph "Docker Host (your server/laptop)"
subgraph "Docker Compose Cluster"
N8N[🤖 n8n Container
Port 5678]
PG[🐘 PostgreSQL Container
Port 5432]
N8N -->|"Stores workflows/credentials/logs"| PG
end
VOL_N8N[📁 n8n_data volume
/home/node/.n8n]
VOL_PG[📁 postgres_data volume
/var/lib/postgresql/data]
N8N -.->|"Persists files/community nodes"| VOL_N8N
PG -.->|"Persists database"| VOL_PG
end
USER[👤 Browser] -->|"https://localhost:5678"| N8N
WEBHOOK[🌐 External Webhook] -->|"POST /webhook/xxx"| N8N
style N8N fill:#ff6d5b,stroke:#e55a4e,color:#fff
style PG fill:#336791,stroke:#2d5a7b,color:#fffStep 1: Create Project Directory
# Create a dedicated n8n working directory
mkdir -p ~/n8n-docker && cd ~/n8n-docker
# Create environment file (IMPORTANT: NEVER put secrets in docker-compose.yml!)
touch .env
Step 2: Write the .env Environment File
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# n8n Docker Environment Variables (.env)
# All sensitive config centralized here; docker-compose.yml references via ${VAR}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ── Core Encryption ──────────────────────────────────
# Master key for AES-256 encryption of all API credentials
# CANNOT be changed after initial setup! All saved credentials become unreadable
# Generate with: openssl rand -hex 32
N8N_ENCRYPTION_KEY=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
# ── Database ─────────────────────────────────────────
# PostgreSQL strongly recommended over default SQLite for production
POSTGRES_USER=n8n
POSTGRES_PASSWORD=your_strong_pw_here # REPLACE THIS!
POSTGRES_DB=n8n_production
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres # Docker internal service name
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
DB_POSTGRESDB_USER=${POSTGRES_USER}
DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
# ── Access Control ───────────────────────────────────
N8N_BASIC_AUTH_ACTIVE=true
N8N_BASIC_AUTH_USER=admin
N8N_BASIC_AUTH_PASSWORD=change_me! # REPLACE THIS!
# ── Network ──────────────────────────────────────────
WEBHOOK_URL=https://n8n.yourdomain.com/
N8N_PROTOCOL=http
N8N_PORT=5678
# ── Timezone ─────────────────────────────────────────
GENERIC_TIMEZONE=Asia/Shanghai
TZ=Asia/Shanghai
# ── Execution Security ──────────────────────────────
# Enable Task Runner (n8n 2.0+): runs Code nodes in isolated sandbox
N8N_RUNNERS_ENABLED=true
Step 3: Write docker-compose.yml
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# n8n Docker Compose Configuration
# Compatible with Docker Compose V2 (no 'version' field needed)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
services:
# ── n8n Main Service ───────────────────────────────
n8n:
image: n8nio/n8n:latest # Official image, always pull latest stable
container_name: n8n
restart: unless-stopped # Auto-restart on crash, not on manual stop
ports:
- "5678:5678" # Port mapping: host 5678 → container 5678
environment:
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- DB_TYPE=${DB_TYPE}
- DB_POSTGRESDB_HOST=${DB_POSTGRESDB_HOST}
- DB_POSTGRESDB_PORT=${DB_POSTGRESDB_PORT}
- DB_POSTGRESDB_DATABASE=${DB_POSTGRESDB_DATABASE}
- DB_POSTGRESDB_USER=${DB_POSTGRESDB_USER}
- DB_POSTGRESDB_PASSWORD=${DB_POSTGRESDB_PASSWORD}
- N8N_BASIC_AUTH_ACTIVE=${N8N_BASIC_AUTH_ACTIVE}
- N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER}
- N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD}
- WEBHOOK_URL=${WEBHOOK_URL}
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- TZ=${TZ}
- N8N_RUNNERS_ENABLED=${N8N_RUNNERS_ENABLED}
volumes:
- n8n_data:/home/node/.n8n # Persist: community nodes, file uploads
depends_on:
postgres:
condition: service_healthy # Wait for PostgreSQL health check before starting
networks:
- n8n_network
# ── PostgreSQL Database ────────────────────────────
postgres:
image: postgres:16-alpine # Alpine variant for smaller footprint
container_name: n8n_postgres
restart: unless-stopped
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
networks:
- n8n_network
volumes:
n8n_data:
postgres_data:
networks:
n8n_network:
driver: bridge # Containers access each other by service name (e.g., postgres:5432)
Step 4: Launch & Verify
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Start the n8n cluster (detached mode)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
docker compose up -d
# Check container status
docker compose ps
# View n8n startup logs (confirm no errors)
docker compose logs n8n --tail 20
# Key line to look for: "n8n ready on 0.0.0.0, port 5678"
# Verify the service responds
curl -s http://localhost:5678/healthz
# Expected: {"status":"ok"}
Startup Sequence Diagram
sequenceDiagram
participant Dev as 👤 Developer
participant DC as 🐳 Docker Compose
participant PG as 🐘 PostgreSQL
participant N8N as 🤖 n8n
Dev->>DC: docker compose up -d
DC->>PG: Start PostgreSQL container
loop Health Check (every 5s)
DC->>PG: pg_isready?
PG-->>DC: Not ready...
end
PG-->>DC: ✅ Ready!
DC->>N8N: Start n8n container
N8N->>PG: Connect to database
N8N->>N8N: Run database migrations
N8N->>N8N: Load encryption key
N8N-->>Dev: 🟢 n8n ready on port 5678
Dev->>N8N: Open browser → localhost:5678
N8N-->>Dev: Show login/registration UITroubleshooting
| Symptom | Likely Cause | Solution |
|---|---|---|
| Container keeps restarting | N8N_ENCRYPTION_KEY not set |
Check if .env file loads correctly |
| Can't log in | Password has special chars escaped by shell | Wrap password in quotes in .env |
| Webhook URL doesn't work | WEBHOOK_URL misconfigured |
Ensure https:// protocol prefix |
| Database connection fails | PostgreSQL not ready | depends_on + healthcheck handles this |
Upgrade & Backup
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Safely upgrade n8n to the latest version
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Step 1: Backup database (build the habit!)
docker exec n8n_postgres pg_dump -U n8n n8n_production > backup_$(date +%Y%m%d).sql
# Step 2: Pull latest images
docker compose pull
# Step 3: Seamless restart (data volumes preserved)
docker compose up -d
# Step 4: Verify version
docker exec n8n n8n --version
Next Episode
Environment is ready! In Ep 04, we dive into n8n's most core (and most misunderstood) concept — JSON Data Items and the implicit loop mechanism — understanding why you almost never need to write for-loops.