Deploy n8n with PostgreSQL on Debian VPS

The default n8n setup with SQLite works fine for testing, but the moment you have a few workflows running on schedule or webhooks hammering the instance, SQLite becomes the bottleneck. Switching to PostgreSQL gives you proper concurrent write handling, point-in-time recovery, and a database you already know how to back up. This guide walks through deploying n8n workflow automation with PostgreSQL on a Debian 12 VPS in Vietnam, using Docker Compose, with Nginx as a reverse proxy in front. The final result: a production-ready n8n instance on a Linux VPS with full root access, reachable over HTTPS.
Prerequisites
- A Debian 12 VPS. The setup in this guide works on 2 GB RAM, but 4 GB is more comfortable if you run many concurrent workflows. thueVPS offers n8n VPS plans starting from 2 GB.
- Root access or a user with sudo privileges.
- A domain name pointing to your VPS IPv4. You need this for HTTPS with Let's Encrypt.
- Basic familiarity with the command line and Docker concepts.
Why run n8n with PostgreSQL instead of SQLite
SQLite is a file-based database. It works, but it locks the entire database on writes. n8n workflows that run in parallel, or a burst of webhook calls, will serialize on those locks. You see timeouts, and the UI stutters.
PostgreSQL handles concurrent connections properly. It also gives you pg_dump for logical backups and WAL archiving for point-in-time recovery. If you are self-hosting n8n for a client or your own business, this matters. Losing a workflow definition because the VPS disk died is not an acceptable outcome.
The official n8n Docker image ships with SQLite by default and it is tempting to skip PostgreSQL. Do not. The extra 15 minutes of setup pays for itself the first time you need to restore a workflow from backup.
Step 1 - Install Docker and Docker Compose on Debian 12
Debian 12 ships with an older Docker package in its repositories. Install Docker from the official Docker repository instead, so you get current versions and security patches.
sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
This installs the Docker daemon, the CLI, and the Compose v2 plugin. Note the command uses docker compose, not the legacy docker-compose.
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
Log out and back in for the group change to take effect, then verify:
docker --version
docker compose version
You should see Docker version 27.x or newer and Compose v2. If you prefer a more detailed walkthrough, see our guide on installing Docker Compose on a Linux VPS.
Step 2 - Set up the project directory and environment file
Create a directory for n8n and a .env file to hold configuration. Keep secrets out of the Compose file.
mkdir -p ~/n8n && cd ~/n8n
nano .env
Paste this into the file, changing the passwords to something strong:
POSTGRES_USER=n8n
POSTGRES_PASSWORD=change_this_password
POSTGRES_DB=n8n
N8N_DB_TYPE=postgresdb
N8N_DB_HOST=postgres
N8N_DB_PORT=5432
N8N_DB_USER=n8n
N8N_DB_PASSWORD=change_this_password
N8N_DB_NAME=n8n
N8N_HOST=n8n.yourdomain.com
N8N_PROTOCOL=https
N8N_PORT=5678
WEBHOOK_URL=https://n8n.yourdomain.com/
The N8N_DB_* variables tell n8n to use PostgreSQL instead of the default SQLite. WEBHOOK_URL ensures callback URLs in workflows use your public domain, which matters when you send webhooks to external services.
Step 3 - Create the Docker Compose file
Still in ~/n8n, create docker-compose.yml:
nano docker-compose.yml
services:
postgres:
image: postgres:16
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: 10s
timeout: 5s
retries: 5
n8n:
image: docker.n8n.io/n8nio/n8n
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
- N8N_DB_TYPE=${N8N_DB_TYPE}
- N8N_DB_HOST=${N8N_DB_HOST}
- N8N_DB_PORT=${N8N_DB_PORT}
- N8N_DB_USER=${N8N_DB_USER}
- N8N_DB_PASSWORD=${N8N_DB_PASSWORD}
- N8N_DB_NAME=${N8N_DB_NAME}
- N8N_HOST=${N8N_HOST}
- N8N_PROTOCOL=${N8N_PROTOCOL}
- N8N_PORT=${N8N_PORT}
- WEBHOOK_URL=${WEBHOOK_URL}
- GENERIC_TIMEZONE=Asia/Ho_Chi_Minh
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:
n8n_data:
Two details matter here. First, n8n listens on 127.0.0.1:5678 only, not on all interfaces. Nginx on the same host will proxy traffic to it. You never expose n8n directly to the internet without TLS. Second, the Postgres container gets a healthcheck, and n8n waits for it before starting. Without this, n8n can start before the database is ready and crash on the first connection attempt.
Pull the images and start the stack:
docker compose up -d
Check that both containers are running:
docker compose ps
You should see both postgres and n8n with status "Up" and "healthy" for postgres.
Step 4 - Configure Nginx as a reverse proxy with HTTPS
Install Nginx if it is not already on the VPS:
sudo apt install -y nginx
Create a site configuration:
sudo nano /etc/nginx/sites-available/n8n
server {
listen 80;
server_name n8n.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
proxy_read_timeout 3600;
}
}
The Upgrade and Connection headers are required for webhooks and for the n8n editor's websocket connections. Without them, some UI features break silently. The proxy_read_timeout 3600 prevents nginx from killing long-running workflow executions.
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Now add TLS with Certbot:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d n8n.yourdomain.com
Certbot modifies the Nginx config automatically and sets up a renewal timer. Verify the certificate is in place:
sudo certbot certificates
You should see a certificate listed with your domain and an expiry date. If you manage multiple domains or services, a wildcard certificate from GoGetSSL can cover them all, see how to automate wildcard SSL with acme.sh for the broader setup.
Step 5 - Open the firewall and verify the deployment
Debian 12 does not enable a firewall by default, but if you use nftables or ufw, allow HTTP and HTTPS:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload
Do not open port 5678. Nginx proxies to it over the loopback interface, and nothing external needs direct access.
Now open your browser and visit https://n8n.yourdomain.com. You will see the n8n owner setup screen. Create your account and you are in.
From the server side, confirm n8n is healthy:
sudo docker compose -f ~/n8n/docker-compose.yml logs n8n --tail 50
Look for a line like "Editor is now accessible via: http://localhost:5678" and no stack traces around database connection errors.
Backing up n8n workflows and PostgreSQL data
Your n8n workflows live in the Postgres database, not in files. Backing up means dumping the database. The n8n data volume only holds encryption keys and settings, keep it too, but the workflows are in Postgres.
A simple cron job can handle nightly dumps:
sudo crontab -e
0 3 * * * docker exec $(docker ps -qf name=n8n-postgres-1) pg_dump -U n8n n8n | gzip > /root/backups/n8n_$(date +\%Y\%m\%d).sql.gz
Adjust the container name to match what docker ps shows. Test the restore path at least once. A backup you never restored is a guess, not a backup. For a more complete strategy, see our guide on backing up n8n workflows and credentials.
Common issues and troubleshooting
n8n container keeps restarting. Almost always a Postgres connection problem. Check the database container first:
docker compose -f ~/n8n/docker-compose.yml logs postgres
Look for authentication failures. The N8N_DB_PASSWORD in .env must match POSTGRES_PASSWORD. If you changed one and not the other, the connection fails.
Webhooks return 502 Bad Gateway. Nginx cannot reach n8n. Confirm n8n is listening on the loopback interface:
ss -tlnp | grep 5678
You should see 127.0.0.1:5678. If it shows 0.0.0.0:5678, your Compose file is missing the 127.0.0.1: prefix on the port mapping.
Certificate renewal fails. The DNS record must point to the VPS IPv4. If you recently migrated the VPS, update the A record and wait for propagation before running certbot again.
Scaling and performance notes
The setup described here handles a serious workload on a 2 GB VPS. PostgreSQL and n8n together use roughly 300-400 MB at rest. Each running workflow adds maybe 50-100 MB depending on what it does. If you run dozens of workflows on 5-minute schedules, upgrade to 4 GB. See our analysis of RAM requirements for multiple n8n workflows for the numbers.
For higher throughput, put a Redis queue between n8n and Postgres. The n8n Enterprise edition supports it, but you can also run a separate Redis container and point n8n at it with QUEUE_MODE=true and REDIS_HOST=redis. This lets you scale workers horizontally. It adds operational complexity, so only do it when you actually outgrow the single-process mode.
All of this runs comfortably on a VPS for automation workflows with a dedicated IPv4. The Vietnam datacenter placement means webhooks hitting Vietnamese services like Zalo, Viettel, or local banks get single-digit millisecond latency instead of crossing international transit. For workloads that touch Vietnamese systems, that local routing is the difference between reliable automation and constant timeouts.
FAQ
Does n8n work with PostgreSQL on a 2 GB VPS?
Yes. PostgreSQL and n8n together use around 300-400 MB at idle. A 2 GB VPS handles a moderate workload. Go with 4 GB if you plan to run many concurrent workflows.
Can I migrate an existing n8n SQLite instance to PostgreSQL?
Not automatically. Export each workflow as JSON from the old instance and import them into the new one. Credentials must be re-entered. Plan for a short maintenance window.
How do I update n8n to a newer version?
Stop the stack, pull the new image, and start it again: docker compose down, then docker compose pull and docker compose up -d. n8n runs database migrations on startup automatically.
Is PostgreSQL on the same VPS a single point of failure?
Yes. For home lab use this is acceptable. For production, take nightly pg_dump backups and test restores. If you need higher availability, run Postgres on a second VPS and configure replication.
Why does n8n need a webhook URL configured?
Workflows that expose webhooks return the URL to external callers. Without WEBHOOK_URL, n8n uses the local hostname, and external services cannot reach it. Set it to your public HTTPS domain.
Related articles
- Self-hosting n8n for workflow automation on a VPS
- How to install n8n on a VPS with Docker in 2026
- Install PostgreSQL on a Debian 12 VPS
- Set up Nginx as a reverse proxy on Ubuntu 24.04
在越南 VPS 上部署 n8n 与 PostgreSQL
本指南在 Debian 12 VPS 上使用 Docker Compose 部署 n8n 自动化工作流,并将默认的 SQLite 数据库替换为 PostgreSQL,以获得更好的并发处理能力和可靠的备份方案。通过 Nginx 反向代理配置 HTTPS,并使用 Certbot 自动续期证书。建议至少 2GB 内存,备份时使用 pg_dump 定期导出数据库。越南本地机房部署对调用本地服务的自动化工作流延迟更低。


