How to Install n8n with Docker Compose (Self-Hosted Setup)
Step-by-step guide to self-host n8n with Docker Compose, configure a PostgreSQL database, set up Nginx reverse proxy, and enable SSL with Let's Encrypt.

n8n is an open-source workflow automation platform with 400+ integrations. It lets you build automations between any API, database, or service using a visual node editor, without writing code for most tasks. The self-hosted version is free with no workflow or execution limits, unlike the cloud version which charges per execution.
This guide covers a production-ready self-hosted n8n deployment: Docker Compose with PostgreSQL for reliable storage, an Nginx reverse proxy, and SSL via Let's Encrypt. You will also configure webhook URLs, set up basic authentication, and learn how to update n8n when new versions release.
By the end, you will have n8n running on your own server, accessible via HTTPS at your domain, with data persisted in a PostgreSQL database.
Prerequisites
- A Linux server or VPS (Ubuntu 22.04 recommended) with at least 1 GB RAM
- Docker Engine 24.x+ installed
- Docker Compose 2.x+ installed (ships with Docker Desktop, or install the Docker Compose plugin)
- A domain name pointing to your server's IP address (for SSL setup)
- Port 80 and 443 open in your firewall
- Basic familiarity with the Linux command line
Need a VPS?
Run this on a Contabo Cloud VPS 10 starting at €5.45/mo. Reliable Linux VPS with NVMe storage, ideal for self-hosted AI workloads.
In This Guide
Create the Project Directory
Start by creating a dedicated directory for your n8n installation and the files it needs.
# Create and enter the project directory
mkdir -p ~/n8n && cd ~/n8nYou will create three files in this directory:
- `docker-compose.yml` — defines the n8n and PostgreSQL services
- `.env` — stores configuration values and credentials
- `nginx.conf` — the Nginx reverse proxy configuration (added later)
Create the Environment File
The `.env` file holds your configuration. Create it now and fill in your values before starting the containers.
nano ~/n8n/.envPaste the following and replace every value in `< >` with your own:
# n8n Core Configuration
N8N_HOST=your-domain.com
N8N_PORT=5678
N8N_PROTOCOL=https
WEBHOOK_URL=https://your-domain.com
# Encryption Key (generate a random 32-character string)
# Run: openssl rand -hex 16
N8N_ENCRYPTION_KEY=your_32_char_random_string_here
# Basic Auth (protects the n8n UI with a login)
N8N_BASIC_AUTH_ACTIVE=true
N8N_BASIC_AUTH_USER=admin
N8N_BASIC_AUTH_PASSWORD=your_secure_password
# PostgreSQL Database
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n
DB_POSTGRESDB_PASSWORD=your_db_password
# Timezone (for scheduled workflows)
GENERIC_TIMEZONE=Europe/London
# Optional: Disable telemetry
N8N_DIAGNOSTICS_ENABLED=false
N8N_PERSONALIZATION_ENABLED=falseGenerate a secure encryption key:
openssl rand -hex 16
# Example output: a3f8c2d1e4b5097634a1f2e3d4c5b6a7Copy that output into the `N8N_ENCRYPTION_KEY` value. This key encrypts stored credentials. If you lose it or change it, all saved credentials become unreadable.
Create the Docker Compose File
Create the `docker-compose.yml` file that defines the n8n and PostgreSQL services:
nano ~/n8n/docker-compose.ymlPaste the following:
version: '3.8'
volumes:
n8n_data:
postgres_data:
services:
postgres:
image: postgres:15-alpine
restart: unless-stopped
environment:
POSTGRES_DB: n8n
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${DB_POSTGRESDB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -h localhost -U n8n -d n8n']
interval: 5s
timeout: 5s
retries: 10
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_BASIC_AUTH_ACTIVE=${N8N_BASIC_AUTH_ACTIVE}
- N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER}
- N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD}
- 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}
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- N8N_DIAGNOSTICS_ENABLED=${N8N_DIAGNOSTICS_ENABLED}
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthyStart n8n
With the `.env` and `docker-compose.yml` files ready, start the services:
cd ~/n8n
docker compose up -dDocker pulls the images (first run takes 1-3 minutes) and starts both containers. Check that both are running:
docker compose psExpected output:
NAME IMAGE COMMAND SERVICE STATUS
n8n-n8n-1 n8nio/n8n:latest "tini -- /docker-ent…" n8n Up 30 seconds
n8n-postgres-1 postgres:15-alpine "docker-entrypoint.s…" postgres Up 32 seconds (healthy)Check the n8n logs for startup confirmation:
docker compose logs n8n | tail -20Look for this line, which confirms n8n started successfully:
n8n ready on 0.0.0.0, port 5678At this point, n8n is accessible at `http://your-server-ip:5678`. The next section adds Nginx and SSL so it is accessible at `https://your-domain.com` instead.
Set Up Nginx Reverse Proxy and SSL
A reverse proxy handles HTTPS termination and forwards requests to n8n running on port 5678. Install Nginx and Certbot:
sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginxCreate the Nginx configuration for your domain:
sudo nano /etc/nginx/sites-available/n8nPaste the following, replacing `your-domain.com` with your actual domain:
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:5678;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (required for n8n UI live updates)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300;
proxy_connect_timeout 300;
}
}Enable the site and test the configuration:
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/n8n
sudo nginx -t
# Expected: nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginxObtain an SSL certificate from Let's Encrypt:
sudo certbot --nginx -d your-domain.comCertbot modifies your Nginx config to add HTTPS and sets up automatic renewal. After it completes, n8n is accessible at `https://your-domain.com`.
Verify the SSL certificate auto-renews:
sudo certbot renew --dry-run
# Expected: Congratulations, all simulated renewals succeededUpdate n8n to a New Version
When n8n releases a new version, update your deployment by pulling the new image and recreating the container:
cd ~/n8n
# Pull the latest image
docker compose pull n8n
# Recreate the n8n container with the new image
# --no-deps: only restart the n8n service, not postgres
docker compose up -d --no-deps n8nCheck the running version after the update:
docker compose exec n8n n8n --versionBackup n8n Data
Back up your n8n data before updates or migrations:
# Export all workflows as JSON
docker compose exec n8n n8n export:workflow --all --output=/home/node/.n8n/workflows-backup.json
# Copy the backup from the container to your host
docker cp n8n-n8n-1:/home/node/.n8n/workflows-backup.json ./workflows-backup.jsonTroubleshooting
n8n container exits immediately on start
Cause: PostgreSQL is not yet ready when n8n tries to connect. This usually happens on the very first run before the healthcheck passes
Fix: Run `docker compose logs postgres` to check if PostgreSQL is starting. The `depends_on: condition: service_healthy` in the compose file should handle this, but if it fails, wait 10 seconds and run `docker compose up -d` again
Cannot connect to n8n at port 5678
Cause: Firewall is blocking port 5678, or n8n is binding to 127.0.0.1 only
Fix: Check if port 5678 is open: `sudo ufw status`. To allow it: `sudo ufw allow 5678`. If using Nginx as a reverse proxy, you do not need to expose port 5678 publicly — remove it from UFW and access n8n through port 443
Webhooks are not receiving requests
Cause: The WEBHOOK_URL in .env does not match the domain where n8n is accessible, so n8n generates incorrect webhook URLs
Fix: Set WEBHOOK_URL to the exact public URL: `WEBHOOK_URL=https://your-domain.com`. Then restart n8n: `docker compose restart n8n`. Recreate any webhook triggers after the URL change
SSL certificate fails to issue ("Connection refused" in Certbot)
Cause: Port 80 is blocked by a firewall, or your domain DNS has not propagated yet
Fix: Open port 80: `sudo ufw allow 80`. Check that your domain resolves to your server IP: `dig your-domain.com`. DNS propagation can take up to 48 hours
Credentials show "Could not decrypt" error after restore or migration
Cause: The N8N_ENCRYPTION_KEY in .env does not match the key used when credentials were saved
Fix: Restore the original .env file with the correct N8N_ENCRYPTION_KEY. This key must match exactly. If the original key is lost, credentials cannot be recovered and must be re-entered
Alternatives to Consider
| Tool | Type | Price | Best For |
|---|---|---|---|
| Zapier | Cloud SaaS | Free tier (100 tasks/month), paid from $19.99/month | Non-technical users who want managed infrastructure with no server maintenance |
| Make (formerly Integromat) | Cloud SaaS | Free tier (1,000 ops/month), paid from $9/month | Teams that need more complex logic than Zapier at a lower cost |
| Activepieces | Self-hosted or cloud | Free (self-hosted), cloud from $0 | n8n alternative with a similar open-source self-hosted model and growing integrations library |
| Pipedream | Cloud SaaS | Free tier (10,000 invocations/month) | Developers who want to write Node.js code in workflows alongside no-code components |
Frequently Asked Questions
Is the self-hosted n8n version free?
Yes. The self-hosted n8n Community Edition is free with no workflow limits, no execution limits, and no feature restrictions. You can run as many workflows with as many executions as your server can handle.
The n8n Cloud pricing applies only to their managed hosting service. When you self-host, you pay only for your server. A Contabo Cloud VPS 10 at €5.45/month handles most personal and small business n8n workloads comfortably.
Why use PostgreSQL instead of SQLite for n8n?
n8n defaults to SQLite but recommends PostgreSQL for production deployments. SQLite stores everything in a single file which can become a bottleneck with high workflow execution volumes and concurrent runs. PostgreSQL handles concurrent access better, supports larger datasets, and has more robust crash recovery.
For personal use with a few workflows, SQLite works fine. For anything handling webhook-triggered workflows at volume (100+ executions per day), use PostgreSQL. The Docker Compose setup in this guide uses PostgreSQL by default because it is the safer choice for growth.
How do I access n8n from outside my network?
The Nginx reverse proxy setup in this guide exposes n8n at your domain over HTTPS, so it is accessible from anywhere once DNS is configured. If you are running n8n on a home server without a public IP, use Cloudflare Tunnel or Ngrok to expose the local port without opening your router's firewall.
For webhook-triggered workflows, n8n must be publicly accessible at the URL set in `WEBHOOK_URL`. Services like Zapier, Stripe, or GitHub send webhook POST requests to that URL when events occur.
Can I run n8n on a server with 512 MB RAM?
n8n runs with as little as 512 MB RAM for basic use, but it is not comfortable. The n8n process itself uses 200-400 MB at idle. Running complex workflows or multiple concurrent executions can push memory usage higher, causing the container to be killed by the OS. 1 GB RAM is the practical minimum for a stable deployment. 2 GB is recommended for workflows that involve large data sets or browser automation steps.
How do I upgrade n8n without losing my workflows?
Workflows are stored in the PostgreSQL database (or SQLite file), not in the Docker container. Updating the container does not affect stored workflows, credentials, or execution history.
Before upgrading, export your workflows as a backup: `docker compose exec n8n n8n export:workflow --all --output=/home/node/.n8n/backup.json`. Then pull the new image and restart: `docker compose pull n8n && docker compose up -d --no-deps n8n`. n8n runs any required database migrations on startup automatically.
What is the difference between n8n and Zapier?
The main differences are cost, privacy, and flexibility. n8n self-hosted is free with no execution limits. Zapier charges per task after the free tier (100 tasks/month). For a business running 10,000+ automated tasks monthly, self-hosted n8n saves hundreds of dollars per month versus Zapier.
n8n supports custom JavaScript and Python code nodes, direct SQL database queries, SSH connections, and complex branching logic. Zapier is designed for simpler linear automations between popular apps. n8n has a steeper learning curve but handles significantly more complex workflows.