Ep 03: Environment Setup — Docker Compose Deployment & Security Configuration

⏱ Est. reading time: 16 min Updated on 4/10/2026

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:#fff

Step 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 UI

Troubleshooting

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.