How to Self-Host n8n with PostgreSQL and Caddy

backend n8n self-hosting docker postgresql caddy automation

n8n is a powerful workflow automation tool you can run entirely on your own infrastructure. In this guide, we’ll walk through setting up n8n on a VPS using Docker Compose, PostgreSQL for storage, and Caddy as a reverse proxy with automatic HTTPS.

The result: a secure, production-ready n8n instance available at your own domain.


Assumptions

This guide assumes you already have:


Directory structure

On your VPS, create a working directory for the stack:


n8n-stack/
├── docker-compose.yml
├── .env
├── init-data.sh
└── Caddyfile

All configuration lives here.


1. Configure environment variables

Create a .env file with your database credentials:

POSTGRES_USER=postgres_admin
POSTGRES_PASSWORD=supersecret
POSTGRES_DB=n8n

POSTGRES_NON_ROOT_USER=n8n_app
POSTGRES_NON_ROOT_PASSWORD=appsecret

This file is used by both Postgres and n8n.


2. Database initialization script

The init-data.sh script runs once, the first time Postgres initializes. It creates a non-root user for n8n and grants privileges:

#!/bin/bash
set -e;

if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then
    psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
        CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}';
        GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER};
        GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER};
    EOSQL
else
    echo "SETUP INFO: No Environment variables given!"
fi

3. Docker Compose setup

docker-compose.yml defines three services:

version: '3.8'

volumes:
  db_storage:
  n8n_storage:
  caddy_data:
  caddy_config:

services:
  postgres:
    image: postgres:16
    restart: always
    environment:
      - POSTGRES_USER
      - POSTGRES_PASSWORD
      - POSTGRES_DB
      - POSTGRES_NON_ROOT_USER
      - POSTGRES_NON_ROOT_PASSWORD
    volumes:
      - db_storage:/var/lib/postgresql/data
      - ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
      interval: 5s
      timeout: 5s
      retries: 10

  n8n:
    image: docker.n8n.io/n8nio/n8n
    restart: always
    environment:
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
      - DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER}
      - DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}

      - N8N_HOST=n8n.example.com
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://n8n.example.com
      - N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true

    expose:
      - 5678
    links:
      - postgres
    volumes:
      - n8n_storage:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy

  caddy:
    image: caddy:2
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - n8n

4. Caddy configuration

The Caddyfile tells Caddy to proxy HTTPS traffic for your domain to the n8n container:

n8n.example.com {
    reverse_proxy n8n:5678
}

Caddy automatically requests and renews TLS certificates from Let’s Encrypt.


5. DNS setup

Point your domain to the VPS:

Once DNS resolves, Caddy will handle certificates automatically.


6. Start the stack

Bring everything up:

docker compose up -d

Stop the stack:

docker compose stop

At this point, you should be able to open https://n8n.example.com in your browser and complete the initial n8n setup.