Skip to Content

n8n Windows Setup WSL2: PostgreSQL Backend, Docker Compose, Webhook Tunneling, and Windows Terminal Integration

Get n8n running on Windows with WSL2 using a production-grade PostgreSQL database, Docker Compose orchestration, live webhook testing via tunnel, and a Windows Terminal profile that makes your local automation stack feel native.

n8n Windows Setup WSL2: PostgreSQL Backend, Docker Compose, Webhook Tunneling, and Windows Terminal Integration

Running n8n on Windows directly — via npm or the desktop app — works for basic testing but falls apart fast: SQLite limits you to single-threaded execution, Windows file paths break shell-based nodes, and there's no clean path to the production setup you'd eventually deploy. WSL2 changes that entirely. With WSL2 you get a real Linux kernel under Windows, Docker Desktop integration that runs containers natively, and a setup that mirrors the production patterns used in our Ubuntu + Docker Compose production guides — so your local workflows run identically in production. This guide sets up n8n on Windows with WSL2 from scratch: WSL2 with Ubuntu, Docker Compose with PostgreSQL, webhook tunneling for testing external triggers, and a Windows Terminal profile that makes the whole stack feel native.


Prerequisites

  • Windows 10 version 2004+ (Build 19041+) or Windows 11 — check with winver
  • 8 GB RAM minimum; 16 GB recommended (WSL2 + Docker + n8n + PostgreSQL)
  • Virtualization enabled in BIOS — check Task Manager → Performance → CPU → Virtualization: Enabled
  • 20 GB free disk space for WSL2 distro, Docker images, and n8n data
  • Administrator access on the Windows machine
  • Windows Terminal installed (from Microsoft Store or GitHub)
# Run these checks in PowerShell (as Administrator) before starting:

# Check Windows version:
[System.Environment]::OSVersion.Version
# Major must be 10, Build must be 19041 or higher

# Check if WSL is already installed:
wsl --status
# If not installed, you'll see "Windows Subsystem for Linux is not installed"

# Check virtualization:
(Get-WmiObject Win32_Processor).VirtualizationFirmwareEnabled
# Must return: True

# Check available disk space:
Get-PSDrive C | Select-Object Used, Free
# Free should be 20GB+ (in bytes, so 20000000000+)

# Check RAM:
(Get-CimInstance Win32_PhysicalMemory | Measure-Object -Property capacity -Sum).Sum / 1GB
# Should be 8+ GB

Installing WSL2 and Ubuntu

WSL2 runs a genuine Linux kernel in a lightweight Hyper-V VM. Unlike WSL1 which translated Linux system calls to Windows equivalents, WSL2 runs the real thing — which means Docker works properly, file system performance in the Linux filesystem is fast, and network behavior matches what you'd see on a real Ubuntu server.

WSL2 Installation

# Open PowerShell as Administrator and run:

# Install WSL2 with Ubuntu (single command on Windows 11 / Win10 21H2+):
wsl --install
# This installs: WSL2 kernel, Ubuntu 22.04 LTS, and sets WSL2 as default
# Reboot when prompted

# --- After reboot ---

# If wsl --install didn't work (older Windows 10), use the manual method:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
# Reboot, then:
wsl --set-default-version 2
# Then install Ubuntu from Microsoft Store

# Verify WSL2 is active after reboot:
wsl --list --verbose
# Should show:
#   NAME      STATE    VERSION
# * Ubuntu    Running  2
#             ^--- VERSION must be 2

# Set Ubuntu as the default distro if not already:
wsl --set-default Ubuntu

# Enter Ubuntu for the first time and create your user account:
wsl
# You'll be prompted for a username and password
# These are Linux credentials, separate from your Windows account

Configuring WSL2 Resource Limits

# WSL2 defaults to using up to 50% of system RAM and all CPU cores
# For a dev machine running n8n + PostgreSQL + Docker, tune this:

# Create the WSL2 global config file (in Windows, not inside WSL):
# Open Notepad or VS Code and save to: C:\Users\YourUsername\.wslconfig

# Or create it from PowerShell:
$wslConfig = @"
[wsl2]
# Maximum RAM WSL2 can use:
memory=6GB

# Number of CPU cores available to WSL2:
processors=4

# Swap file size:
swap=2GB

# Enable localhost forwarding (access WSL2 services via localhost on Windows):
localhostForwarding=true

# Faster DNS resolution:
[network]
generateResolvConf=true
"@

$wslConfig | Out-File -FilePath "$env:USERPROFILE\.wslconfig" -Encoding UTF8

# Restart WSL2 to apply:
wsl --shutdown
wsl

# Verify from inside WSL2:
free -h
# Should reflect the memory limit you set
nproc
# Should reflect processor count

Installing Docker Desktop with WSL2 Backend

Docker Desktop on Windows uses WSL2 as its backend — containers run inside the WSL2 VM, so they behave identically to containers on a Linux server. This means the exact same Docker Compose files used in the Ubuntu + Nginx production guide and Traefik production guide work on your Windows machine without modification.

# Install Docker Desktop for Windows:
# 1. Download from: https://www.docker.com/products/docker-desktop/
# 2. Run the installer — it detects WSL2 automatically
# 3. During install: check "Use WSL 2 based engine" (should be pre-checked)
# 4. Restart when prompted

# After Docker Desktop starts, enable WSL2 integration:
# Docker Desktop → Settings → Resources → WSL Integration
# Toggle ON for your Ubuntu distro

# Verify Docker works from inside WSL2:
wsl
docker --version
# Docker version 24.x or higher

docker compose version
# Docker Compose version v2.x or higher

# Test container runs correctly:
docker run --rm hello-world
# Should print: Hello from Docker!

# Verify Docker is using WSL2 backend (from PowerShell):
docker info | Select-String "OS/Arch"
# Should show: OS/Arch: linux/amd64 (NOT windows/amd64)

# Check resource allocation inside WSL2:
docker system info | grep -E '(CPUs|Total Memory)'
# Should reflect your .wslconfig limits

n8n with Docker Compose and PostgreSQL

Running n8n with SQLite (the default) is fine for experimenting, but it doesn't support concurrent executions and has no connection pooling. PostgreSQL gives you proper transaction isolation, concurrent workflow execution, and a database that can handle real production load — identical to what the Caddy + PostgreSQL + Redis production guide uses. Set this up now and your local config is a direct mirror of production.

# All commands from inside WSL2 Ubuntu:

# Create the n8n project directory inside the Linux filesystem
# IMPORTANT: keep project files in ~/... not /mnt/c/...
# Files under /mnt/c/ (Windows filesystem) are 10-100x slower due to filesystem translation
mkdir -p ~/n8n-local && cd ~/n8n-local

# Create the environment file:
cat > .env << 'EOF'
# n8n Configuration
N8N_HOST=localhost
N8N_PORT=5678
N8N_PROTOCOL=http
WEBHOOK_URL=http://localhost:5678/

# Security
N8N_ENCRYPTION_KEY=your-32-char-encryption-key-here
N8N_USER_MANAGEMENT_JWT_SECRET=your-jwt-secret-here

# Database
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n
DB_POSTGRESDB_PASSWORD=n8n_local_password_change_me
DB_POSTGRESDB_SCHEMA=public

# Execution
EXECUTIONS_PROCESS=main
EXECUTIONS_DATA_SAVE_ON_ERROR=all
EXECUTIONS_DATA_SAVE_ON_SUCCESS=last
EXECUTIONS_DATA_PRUNE=true
EXECUTIONS_DATA_MAX_AGE=168

# Timezone
GENERIC_TIMEZONE=America/New_York

# Logging
N8N_LOG_LEVEL=info
N8N_LOG_OUTPUT=console
EOF

# Generate proper secrets:
echo "N8N_ENCRYPTION_KEY=$(openssl rand -hex 16)"
echo "N8N_USER_MANAGEMENT_JWT_SECRET=$(openssl rand -hex 32)"
# Replace the placeholder values in .env with these outputs

# Create docker-compose.yml:
cat > docker-compose.yml << 'COMPOSE'
version: "3.8"

services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${DB_POSTGRESDB_USER}
      POSTGRES_PASSWORD: ${DB_POSTGRESDB_PASSWORD}
      POSTGRES_DB: ${DB_POSTGRESDB_DATABASE}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_POSTGRESDB_USER} -d ${DB_POSTGRESDB_DATABASE}"]
      interval: 10s
      timeout: 5s
      retries: 5

  n8n:
    image: n8nio/n8n:latest
    restart: unless-stopped
    ports:
      - "5678:5678"
    environment:
      - N8N_HOST=${N8N_HOST}
      - N8N_PORT=${N8N_PORT}
      - N8N_PROTOCOL=${N8N_PROTOCOL}
      - WEBHOOK_URL=${WEBHOOK_URL}
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - N8N_USER_MANAGEMENT_JWT_SECRET=${N8N_USER_MANAGEMENT_JWT_SECRET}
      - 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}
      - DB_POSTGRESDB_SCHEMA=${DB_POSTGRESDB_SCHEMA}
      - EXECUTIONS_PROCESS=${EXECUTIONS_PROCESS}
      - EXECUTIONS_DATA_SAVE_ON_ERROR=${EXECUTIONS_DATA_SAVE_ON_ERROR}
      - EXECUTIONS_DATA_SAVE_ON_SUCCESS=${EXECUTIONS_DATA_SAVE_ON_SUCCESS}
      - EXECUTIONS_DATA_PRUNE=${EXECUTIONS_DATA_PRUNE}
      - EXECUTIONS_DATA_MAX_AGE=${EXECUTIONS_DATA_MAX_AGE}
      - GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
      - N8N_LOG_LEVEL=${N8N_LOG_LEVEL}
      - N8N_LOG_OUTPUT=${N8N_LOG_OUTPUT}
    volumes:
      - n8n-data:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  postgres-data:
  n8n-data:
COMPOSE

# Start the stack:
docker compose up -d

# Tail logs until n8n is ready:
docker compose logs -f n8n 2>&1 | grep -m 1 "Editor is now accessible"

# Open n8n in your Windows browser:
# http://localhost:5678
# (WSL2 localhost forwarding makes this work automatically)

Webhook Tunneling for Local Testing

Webhooks are n8n's primary integration trigger — Stripe, GitHub, Slack, and hundreds of other services send HTTP POST requests to a URL when something happens. On a local WSL2 machine, your n8n isn't reachable from the internet, so webhook testing requires a tunnel: a public URL that forwards requests to your local n8n instance. This section sets up two options — cloudflared for a stable Cloudflare-backed tunnel, and ngrok for quick ad-hoc testing.

# Option A: Cloudflare Tunnel (stable, free, no account required for quick tunnels)

# Install cloudflared in WSL2:
curl -L --output cloudflared.deb \
  https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
rm cloudflared.deb

# Start a quick tunnel to n8n:
cloudflared tunnel --url http://localhost:5678
# Output will include a line like:
# https://your-subdomain.trycloudflare.com
# Copy this URL

# Update n8n's webhook URL to use the tunnel:
# Edit .env:
VISIBLE_URL="https://your-subdomain.trycloudflare.com"
sed -i "s|WEBHOOK_URL=.*|WEBHOOK_URL=${VISIBLE_URL}/|" .env

# Restart n8n to pick up the new webhook URL:
docker compose up -d n8n

# Verify n8n is using the tunnel URL:
docker compose exec n8n env | grep WEBHOOK_URL

# ---

# Option B: ngrok (requires free account, gives consistent subdomain on paid plan)

# Install ngrok in WSL2:
curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | \
  sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | \
  sudo tee /etc/apt/sources.list.d/ngrok.list
sudo apt update && sudo apt install ngrok

# Authenticate (get your token from https://dashboard.ngrok.com):
ngrok config add-authtoken YOUR_NGROK_TOKEN

# Start tunnel:
ngrok http 5678
# Outputs: Forwarding https://xxxx.ngrok-free.app -> http://localhost:5678

# ---

# For persistent webhook testing, create a helper script:
cat > ~/n8n-local/start-with-tunnel.sh << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

cd ~/n8n-local

# Start n8n stack:
docker compose up -d
echo "Waiting for n8n..."
until curl -sf http://localhost:5678/healthz > /dev/null 2>&1; do
  sleep 2
done

# Start tunnel and capture URL:
echo "Starting Cloudflare tunnel..."
cloudflared tunnel --url http://localhost:5678 &
TUNNEL_PID=$!

# Wait for tunnel URL to appear:
sleep 5
echo "n8n is running at: http://localhost:5678"
echo "Tunnel started (check cloudflared output for public URL)"
echo "Set WEBHOOK_URL in .env to the tunnel URL and restart n8n"

trap "kill $TUNNEL_PID" EXIT
wait $TUNNEL_PID
SCRIPT
chmod +x ~/n8n-local/start-with-tunnel.sh

Windows Terminal Integration

Switching between PowerShell and a WSL2 session to manage n8n gets old fast. Windows Terminal lets you set up a dedicated n8n profile that opens directly in your project directory with a custom appearance — a clean visual separation between your Windows work and your n8n automation environment. Combined with a startup script, opening the n8n terminal tab starts the whole stack automatically.

# Create a startup script that launches n8n and shows status:
cat > ~/n8n-local/wt-startup.sh << 'SCRIPT'
#!/usr/bin/env bash
# Windows Terminal startup script for n8n development

cd ~/n8n-local

# Banner:
echo -e "\033[38;5;99m"
echo " ___  ___"
echo "|__ \|__ \\"
echo "   ) |  ) |"
echo "  / /  / /"
echo " |_|  |_|  local dev"
echo -e "\033[0m"
echo ""

# Check if stack is running:
N8N_STATUS=$(docker compose ps --format json 2>/dev/null | \
  python3 -c "import sys,json; \
  services=[json.loads(l) for l in sys.stdin if l.strip()]; \
  running=[s for s in services if s.get('State')=='running']; \
  print(f'{len(running)}/{len(services)} services running')" 2>/dev/null || echo "stack not running")

echo "Stack: ${N8N_STATUS}"

# Auto-start if not running:
if echo "${N8N_STATUS}" | grep -q "0/"; then
  echo "Starting n8n stack..."
  docker compose up -d
  echo "Started. Waiting for health check..."
  until curl -sf http://localhost:5678/healthz >/dev/null 2>&1; do
    sleep 2
    echo -n "."
  done
  echo ""
fi

# Quick status:
echo ""
echo "n8n:      http://localhost:5678"
echo "Postgres: localhost:5432 (internal)"
echo ""
echo "Commands:"
echo "  dc up -d         Start stack"
echo "  dc down          Stop stack"
echo "  dc logs -f n8n   Follow n8n logs"
echo "  dc ps            Show service status"
echo ""

# Handy aliases:
alias dc='docker compose'
alias n8n-logs='docker compose logs -f n8n'
alias n8n-restart='docker compose restart n8n'
alias n8n-update='docker compose pull n8n && docker compose up -d n8n'

exec bash --norc
SCRIPT
chmod +x ~/n8n-local/wt-startup.sh

# ---

# Add this profile to Windows Terminal:
# Open Windows Terminal → Settings (Ctrl+,) → Open JSON file
# Add to the "profiles" → "list" array:

# {
#   "name": "n8n Dev",
#   "commandline": "wsl.exe -d Ubuntu -- bash --rcfile /root/n8n-local/wt-startup.sh",
#   "startingDirectory": "//wsl.localhost/Ubuntu/root/n8n-local",
#   "icon": "https://n8n.io/favicon.ico",
#   "colorScheme": "One Half Dark",
#   "tabTitle": "n8n",
#   "suppressApplicationTitle": true,
#   "font": {
#     "face": "Cascadia Code",
#     "size": 12
#   }
# }

# If your WSL2 user is not root, adjust the paths above:
# Replace /root/n8n-local with /home/yourusername/n8n-local

Tips, Gotchas, and Troubleshooting

# 1. n8n can't connect to PostgreSQL: "ECONNREFUSED" or "connect ETIMEDOUT"

# Check that postgres container is healthy:
docker compose ps
# State for postgres should be "running (healthy)" not "running"

# Check postgres logs:
docker compose logs postgres | tail -20
# Common cause: wrong password in .env

# Test connection manually from n8n container:
docker compose exec n8n sh -c \
  'nc -zv postgres 5432 && echo connected || echo failed'

# ---

# 2. localhost:5678 not accessible from Windows browser

# WSL2 localhost forwarding sometimes needs a restart:
wsl --shutdown
# Then reopen Windows Terminal and start docker compose again

# If still failing, find WSL2 IP and use it directly:
wsl hostname -I
# Use the first IP: http://172.x.x.x:5678

# For a permanent fix, add a Windows hosts entry (PowerShell as Admin):
Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "127.0.0.1 n8n.local"
# Then access via http://n8n.local:5678

# ---

# 3. Docker Desktop is running but "docker" not found in WSL2

# Check WSL integration is enabled:
# Docker Desktop → Settings → Resources → WSL Integration → Ubuntu: ON
# Then restart Docker Desktop

# Verify from WSL2:
which docker
# Should return: /usr/bin/docker
# If empty: WSL integration is not enabled or Docker Desktop isn't running

# ---

# 4. Workflows run slowly or n8n is sluggish

# Check WSL2 memory consumption:
cat /proc/meminfo | grep -E '(MemTotal|MemAvailable|MemFree)'

# Check Docker resource usage:
docker stats --no-stream

# Increase .wslconfig memory limit (edit C:\Users\You\.wslconfig):
# memory=8GB
# Then: wsl --shutdown

# ---

# 5. "Error: ENOENT: no such file or directory" in n8n Execute Command node

# WSL2 containers use Linux paths, not Windows paths
# WRONG: C:\Users\You\data.csv
# RIGHT: /home/yourusername/data.csv
# To access Windows files from WSL2: /mnt/c/Users/You/data.csv

# ---

# 6. n8n container keeps restarting

# Check for startup errors:
docker compose logs n8n 2>&1 | grep -i 'error\|fatal' | head -20

# Most common cause on first run: encryption key not set or too short
# N8N_ENCRYPTION_KEY must be exactly 32 hex characters:
python3 -c "import secrets; print(secrets.token_hex(16))"
# Paste output into .env as N8N_ENCRYPTION_KEY, restart

# ---

# 7. WSL2 running out of disk space

# Check WSL2 disk usage:
df -h /

# Prune unused Docker images and volumes:
docker system prune -af --volumes

# WSL2 disk image doesn't automatically shrink after deletion
# Compact it from PowerShell (requires wsl --shutdown first):
# wsl --shutdown
# diskpart → select vdisk file="C:\...\ext4.vhdx" → compact vdisk

Pro Tips

  • Always keep your n8n project files in the Linux filesystem (~/n8n-local), never under /mnt/c/ — WSL2 translates every file operation that crosses the filesystem boundary, and reads/writes to Windows paths from inside Linux are 10-50x slower than native Linux filesystem access. Docker volume mounts work correctly and at full speed only when the files are in the Linux filesystem. This is the single most common cause of slow Docker performance on WSL2.
  • Set wsl --shutdown as a habit when you're done for the day — WSL2 doesn't automatically release memory back to Windows when containers stop. Running wsl --shutdown terminates the WSL2 VM and immediately returns all its reserved memory to Windows. On an 8GB machine with other applications running, this matters. It also ensures a clean startup state next session.
  • Use the same Docker Compose structure locally as you'll use in production — when you're ready to deploy, the Traefik + PostgreSQL production guide uses the same service names, volume patterns, and environment variable names as this local setup. Migrating means adding a reverse proxy service and switching a few environment variables — not rewriting your config from scratch.
  • Back up your N8N_ENCRYPTION_KEY immediately and never change it — n8n uses this key to encrypt all stored credentials. If it changes or is lost, every credential in your database becomes unreadable. Store it in a password manager the moment you generate it, before you even run docker compose up for the first time.

Wrapping Up

WSL2 turns Windows into a first-class n8n development environment. Your Docker Compose stack runs natively on Linux, PostgreSQL gives you a production-grade database from day one, webhook tunneling lets you test external integrations without deploying anything, and the Windows Terminal profile keeps everything accessible without switching mental contexts. When you're ready to take workflows to production, the gap from this local setup to the full Caddy + PostgreSQL + Redis production deployment is smaller than you'd expect — the fundamentals are the same.


Need a Production n8n Deployment for Your Team?

Moving from a WSL2 local setup to a hardened production n8n deployment with high availability, queue mode workers, PostgreSQL with connection pooling, TLS termination, and enterprise authentication — the sysbrix team handles the full deployment lifecycle, from initial VPS configuration through ongoing upgrades and workflow backup strategies.

Talk to Us →
How to Deploy Glances with Docker Compose and Nginx Proxy Manager (Production Guide)
Build a secure, browser-based server observability stack with Glances, Docker Compose, and Nginx Proxy Manager.