Self-Host n8n with Docker Compose, PostgreSQL, and Ollama: Private AI-Powered Workflow Automation

DevOps engineer configuring self-hosted n8n workflow automation with Docker Compose, PostgreSQL, and local Ollama AI in a modern homelab
Self-Host n8n with Docker Compose, PostgreSQL, and Ollama: Private AI-Powered Workflow Automation 3

Modern workflow automation has evolved far beyond basic webhook forwarders and cron job schedulers. With the rise of advanced reasoning models and agentic workflows, infrastructure engineers, DevOps specialists, and security-conscious organizations require automation platforms that can integrate deeply with on-premises tools and local language models—without streaming sensitive operational data, credentials, and customer records to external proprietary cloud providers.

n8n is an industry-leading, fair-code workflow automation tool that offers native AI agent capabilities, visual canvas node editing, and hundreds of integrations. While n8n offers a managed cloud service and a default SQLite deployment, deploying n8n in production requires an enterprise-ready relational database like PostgreSQL, secure reverse proxy termination via Caddy, and local inference execution through Ollama (leveraging high-efficiency models like Qwen 3.6, Llama 3.3, and DeepSeek-R1).

In this comprehensive guide, you will build a production-grade, self-hosted n8n automation stack using Docker Compose, hardened PostgreSQL persistence, automated HTTPS certificate management, and native integration with a local Ollama AI engine.

Architectural Overview: The Self-Hosted Automation Stack

When running n8n in production, relying on SQLite causes concurrency bottlenecks, database lock collisions, and data loss risks during rapid execution spikes. Isolating the workflow engine, persistent database, reverse proxy, and local LLM runtime across a dedicated Docker network guarantees fault isolation and zero data leakage.

+-----------------------------------------------------------------------+
|                         Public Internet / LAN                         |
+-----------------------------------------------------------------------+
                                    |
                            HTTPS:443 (TLS)
                                    v
+-----------------------------------------------------------------------+
|  Caddy Reverse Proxy (Automatic Let's Encrypt / Zero Open Local Ports)|
+-----------------------------------------------------------------------+
                                    |
                    Internal Reverse Proxy Network
                                    v
+-----------------------------------------------------------------------+
|  n8n Automation Engine Container (Port 5678)                          |
|  - Webhook Listener & Trigger Workers                                 |
|  - LangChain-based AI Agent & Chain Nodes                             |
|  - Cryptographic Encryption Key & Persistent Credentials              |
+-----------------------------------+-----------------------------------+
                                    |
         +--------------------------+--------------------------+
         | Internal Docker Network                             | Internal Host/Network
         v                                                     v
+-----------------------------------+ +---------------------------------+
| PostgreSQL 16 Alpine Database     | | Ollama LLM Runtime Engine       |
| - Relational State Storage        | | - Models: Qwen 3.6, Llama 3.3   |
| - Execution History & Queues      | | - Local GPU/CPU Inference       |
| - ACID-Compliant Persistence      | | - REST API: Port 11434          |
+-----------------------------------+ +---------------------------------+

Key highlights of this architecture include:

  • n8n Core: Executes triggers, polling loops, custom JavaScript/Python transformations, and external API requests.
  • PostgreSQL 16: Provides rock-solid ACID compliance, connection pooling, and multi-version concurrency control (MVCC) for high-frequency executions.
  • Ollama: Serves cutting-edge local language models and embeddings over a standardized REST API, preventing third-party token leaks and subscription fees.
  • Caddy: Handles automatic TLS certificates, modern HTTP/3 protocols, and reverse proxying with minimal configuration complexity.

Prerequisites & System Preparation

Before deploying the containers, ensure your host environment meets the minimum hardware and software requirements:

  • Operating System: Ubuntu 24.04 LTS, Debian 12, or any modern enterprise Linux distribution.
  • CPU & RAM: Minimum 4 vCPUs and 8 GB RAM (16 GB+ recommended if running 7B-14B parameter models simultaneously via Ollama on CPU/GPU).
  • Storage: At least 40 GB NVMe SSD storage for Docker volumes, execution logs, and quantized model weights.
  • Software: Docker Engine 26.0+ and Docker Compose v2.20+.
  • Domain / DNS: A public A or CNAME DNS record pointing to your server’s IP address (e.g., n8n.example.com).

Update your base packages and install prerequisite utilities:

sudo apt update && sudo apt upgrade -y
sudo apt install -y curl git jq ufw htop ca-certificates gnupg

Create a dedicated directory structure for the automation stack:

sudo mkdir -p /opt/n8n-stack/{data/n8n,data/postgres,data/caddy_data,data/caddy_config}
cd /opt/n8n-stack

Step 1: Environment Configuration (.env)

Store all sensitive database passwords, encryption keys, and domain parameters in a tightly scoped .env file. Generating a strong encryption key is vital because n8n uses it to encrypt all API keys, OAuth tokens, and webhook secrets stored in the database.

Generate secure random strings using openssl:

ENCRYPTION_KEY=$(openssl rand -hex 32)
DB_PASSWORD=$(openssl rand -hex 24)
echo "Generated credentials successfully."

Now create /opt/n8n-stack/.env:

# ------------------------------------------------------------------------------
# DOMAIN & NETWORK SETTINGS
# ------------------------------------------------------------------------------
DOMAIN_NAME=n8n.example.com
SUBDOMAIN=n8n
GENERIC_TIMEZONE=UTC

# ------------------------------------------------------------------------------
# POSTGRESQL DATABASE CREDENTIALS
# ------------------------------------------------------------------------------
POSTGRES_USER=n8n_admin
POSTGRES_PASSWORD=replace_with_strong_password_here
POSTGRES_DB=n8n_production
POSTGRES_NON_ROOT_USER=n8n_admin

# ------------------------------------------------------------------------------
# N8N ENGINE CONFIGURATION
# ------------------------------------------------------------------------------
N8N_ENCRYPTION_KEY=replace_with_generated_encryption_key_here
N8N_HOST=n8n.example.com
N8N_PORT=5678
N8N_PROTOCOL=https
WEBHOOK_URL=https://n8n.example.com/

# Execution Pruning & Performance
EXECUTIONS_DATA_PRUNE=true
EXECUTIONS_DATA_MAX_AGE=168
EXECUTIONS_DATA_PRUNE_MAX_COUNT=50000
N8N_PAYLOAD_SIZE_MAX=64

# Security Hardening
N8N_SECURE_COOKIE=true
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
N8N_DISABLE_PRODUCTION_MAIN_PROCESS=false

# ------------------------------------------------------------------------------
# LOCAL OLLAMA CONFIGURATION
# ------------------------------------------------------------------------------
OLLAMA_HOST=http://host.docker.internal:11434

Lock down permissions so only the root user can read this file:

sudo chmod 600 /opt/n8n-stack/.env

Step 2: Production Docker Compose Specification

Create the docker-compose.yml file. This definition bundles PostgreSQL 16, n8n, and Caddy into an isolated bridge network, while configuring extra_hosts to allow n8n to resolve host.docker.internal and access Ollama running directly on the host (or in an adjacent container).

services:
  postgres:
    image: postgres:16-alpine
    container_name: n8n_postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    networks:
      - n8n_internal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2048M

  n8n:
    image: docker.n8n.io/n8nio/n8n:latest
    container_name: n8n_core
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    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_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - N8N_HOST=${N8N_HOST}
      - N8N_PORT=${N8N_PORT}
      - N8N_PROTOCOL=${N8N_PROTOCOL}
      - WEBHOOK_URL=${WEBHOOK_URL}
      - GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
      - EXECUTIONS_DATA_PRUNE=${EXECUTIONS_DATA_PRUNE}
      - EXECUTIONS_DATA_MAX_AGE=${EXECUTIONS_DATA_MAX_AGE}
      - EXECUTIONS_DATA_PRUNE_MAX_COUNT=${EXECUTIONS_DATA_PRUNE_MAX_COUNT}
      - N8N_PAYLOAD_SIZE_MAX=${N8N_PAYLOAD_SIZE_MAX}
      - N8N_SECURE_COOKIE=${N8N_SECURE_COOKIE}
      - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=${N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS}
    volumes:
      - ./data/n8n:/home/node/.n8n
    networks:
      - n8n_internal
      - n8n_public
    extra_hosts:
      - "host.docker.internal:host-gateway"
    deploy:
      resources:
        limits:
          cpus: '4.0'
          memory: 4096M

  caddy:
    image: caddy:2.8-alpine
    container_name: n8n_caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./data/caddy_data:/data
      - ./data/caddy_config:/config
    networks:
      - n8n_public
    depends_on:
      - n8n

networks:
  n8n_internal:
    driver: bridge
    internal: true
  n8n_public:
    driver: bridge

Step 3: Caddy Reverse Proxy Configuration

Create the Caddyfile in /opt/n8n-stack/Caddyfile. Caddy automatically provisions and renews TLS certificates via Let’s Encrypt or ZeroSSL while enforcing modern TLS ciphers and streaming headers required for n8n webhooks and Server-Sent Events (SSE):

n8n.example.com {
    encode gzip zstd

    # Security Headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "strict-origin-when-cross-origin"
    }

    # Proxy to n8n container
    reverse_proxy n8n:5678 {
        flush_interval -1
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }
}

Ensure file ownership for the n8n container data volume. The n8n container runs as non-root user node with UID/GID 1000:1000:

sudo chown -R 1000:1000 /opt/n8n-stack/data/n8n
sudo chmod 700 /opt/n8n-stack/data/n8n

Step 4: Launching and Initializing the Stack

With configurations set, pull the images and launch the containers in detached mode:

docker compose pull
docker compose up -d

Verify that all services are healthy and running:

docker compose ps

Inspect the initial startup logs to ensure database migrations were applied cleanly:

docker compose logs -f n8n

You should observe the database initialization routine followed by the ready message: Editor is now accessible via: https://n8n.example.com:5678/.

Step 5: Integrating Local AI (Ollama) with n8n AI Nodes

Once you access your n8n web interface at https://n8n.example.com, complete the initial owner account registration. Now, connect n8n to your local Ollama instance without exposing Ollama to the public internet.

Verifying Host-Level Ollama Accessibility

Ensure Ollama is listening on all interfaces (or bound to the Docker gateway interface 172.17.0.1) on the host machine. By default, systemd installations of Ollama bind exclusively to 127.0.0.1. To allow Docker containers to communicate with it, update the systemd unit:

sudo systemctl edit ollama.service

Add the following environment variable override:

[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"

Reload and restart Ollama, then pull the target reasoning and embedding models:

sudo systemctl daemon-reload
sudo systemctl restart ollama

# Pull modern high-efficiency models
ollama pull qwen2.5:14b
ollama pull nomic-embed-text

Test connectivity from inside the n8n container:

docker exec -it n8n_core curl -s http://host.docker.internal:11434/api/tags | jq .

Configuring Ollama Credentials in n8n

  1. In the n8n sidebar, navigate to Credentials > Add Credential.
  2. Search for Ollama.
  3. Set the Base URL to http://host.docker.internal:11434.
  4. Click Save. n8n will test the endpoint and display a green verification badge.
  5. Create a new workflow, add an AI Agent node or Ollama Model sub-node, and select qwen2.5:14b from the dropdown list.

Production Hardening & Operational Best Practices

Running automation workflows that process business logic requires proactive operational controls. Implement these critical safeguards immediately:

1. Automated Database Backups

Create a backup script /opt/n8n-stack/backup.sh to dump the PostgreSQL database without downtime:

#!/usr/bin/env bash
set -eo pipefail

BACKUP_DIR="/var/backups/n8n"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
mkdir -p "$BACKUP_DIR"

# Source environment variables
source /opt/n8n-stack/.env

# Execute pg_dump directly through container
docker exec n8n_postgres pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" | gzip > "${BACKUP_DIR}/n8n_backup_${TIMESTAMP}.sql.gz"

# Rotate backups older than 14 days
find "$BACKUP_DIR" -type f -name "*.sql.gz" -mtime +14 -delete
echo "Backup completed: ${BACKUP_DIR}/n8n_backup_${TIMESTAMP}.sql.gz"

Schedule this script via crontab -e to run daily at 02:00 AM:

0 2 * * * /bin/bash /opt/n8n-stack/backup.sh >> /var/log/n8n-backup.log 2>&1

2. Execution Log Pruning

Without aggressive pruning, high-frequency webhooks will inflate PostgreSQL disk usage by gigabytes weekly. Ensure the variables EXECUTIONS_DATA_PRUNE=true and EXECUTIONS_DATA_MAX_AGE=168 (7 days) remain active in your .env file. This maintains fast UI load times and prevents disk exhaustion.

3. Firewall Rules (UFW)

Only ports 80 and 443 should ever be accessible externally. PostgreSQL (5432), n8n (5678), and Ollama (11434) must remain strictly internal:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw enable

Troubleshooting Common Failure Modes

Issue 1: Webhook URL Mismatch and SSL Handshake Errors

Symptom: Webhooks trigger in the editor during testing, but production webhooks from external services (e.g., GitHub, Stripe) fail with 404 Not Found or SSL Certificate Error.

Root Cause: The WEBHOOK_URL environment variable is unset or points to an internal HTTP port instead of the public HTTPS URL.

Resolution: Explicitly verify in .env that WEBHOOK_URL=https://n8n.example.com/ includes the trailing slash and exact protocol. Restart the n8n container with docker compose up -d n8n.

Issue 2: Container Cannot Connect to Host Ollama Engine

Symptom: n8n AI nodes throw ECONNREFUSED 127.0.0.1:11434 or getaddrinfo ENOTFOUND host.docker.internal.

Root Cause: On Linux, host.docker.internal is not defined automatically unless explicitly declared via extra_hosts in Docker Compose, or Ollama is listening only on loopback.

Resolution: Ensure extra_hosts: ["host.docker.internal:host-gateway"] is configured in docker-compose.yml. Test the resolution using docker exec -it n8n_core getent hosts host.docker.internal. Additionally, verify that Ollama is bound to 0.0.0.0:11434 via netstat -tlpn | grep 11434.

Issue 3: Database Migration Locks After Hard Restart

Symptom: The n8n container restarts continuously, logging MigrationLockError: Resource is locked.

Root Cause: If the n8n container was killed abruptly during a schema update or container restart, TypeORM leaves a lock entry in the PostgreSQL migrations table.

Resolution: Connect directly to the PostgreSQL container and clear the lock table:

docker exec -it n8n_postgres psql -U n8n_admin -d n8n_production -c "DELETE FROM migrations_lock;"
docker compose restart n8n

Conclusion

By deploying n8n on top of PostgreSQL, Caddy, and Ollama, you have built an enterprise-grade automation powerhouse that preserves total data sovereignty. Workflows can process sensitive credentials, execute arbitrary API payloads, and query local LLMs without relying on recurring SaaS subscriptions or compromising data privacy.

From here, you can install community nodes, construct complex multi-agent reasoning chains with LangChain, and orchestrate zero-trust DevOps pipelines entirely within your own self-hosted infrastructure.