AI Automation

How to Install n8n on a VPS with Docker in 2026

You have a fresh VPS and you want to run n8n on it. Not the drag-and-drop demo, the real thing: a production instance with a stable webhook URL, Postgres instead of SQLite, and HTTPS in front. This guide walks through exactly that, starting from a bare Ubuntu 24.04 server, and you can copy-paste every command. You will learn how to install n8n on a VPS with Docker using docker-compose, put Nginx in front of it, secure it with Let's Encrypt, and set up a backup routine that actually restores.

  • Key takeaways
  • Use docker-compose, not a single docker run. You need Nginx, an SSL cert and a way to restart cleanly.
  • Run Postgres as the n8n database. SQLite works only for local demos and breaks under concurrent workflow execution.
  • Set WEBHOOK_URL to your public HTTPS domain, or external triggers will call the wrong host.
  • Back up the .n8n folder and the Postgres database together. Workflows alone are useless without their credentials.

使用 Docker 在越南 VPS 上部署 n8n,可以获得稳定的 Webhook 地址和生产级数据库。

Deploying n8n with Docker on a Vietnam VPS gives you a stable webhook URL and a production-grade database.

Prerequisites

Before you start, make sure you have the following:

  • A Linux VPS running Ubuntu 24.04 LTS or Debian 12. Any provider works, as long as you have root or sudo access.
  • A domain name pointing to the VPS public IP. Create an A record for n8n.yourdomain.com and wait for DNS to propagate.
  • Ports 80 and 443 open in the firewall. Port 5678 is the default n8n port and should NOT be exposed publicly, Nginx will proxy to it.
  • Basic command-line comfort. You will edit files with nano or vim and run systemctl commands.

If you are renting a n8n VPS from a provider like thueVPS, the OS is installed from the control panel and you get full root access over SSH. Choose Ubuntu 24.04 during the OS reinstall step, then continue here.

Why run n8n in Docker instead of a bare install

You can install n8n directly with npm. It works, for a while. Then you upgrade Node, or a dependency breaks, or you move to another server and the whole thing takes an afternoon. Docker isolates n8n, its Node runtime and its database into containers. You define everything in one docker-compose.yml, and the same file runs on your laptop and on a 16 GB production box. That alone is worth it.

There is a second reason: n8n ships frequent updates, roughly every week. With Docker you pull a new image, recreate the container and roll back if something breaks. A bare npm install turns every update into a small ceremony. For a Linux VPS that you manage yourself, Docker Compose is the lowest-friction path.

Step 1 - Install Docker and Docker Compose

On Ubuntu 24.04, install Docker from the official repository, not the distro package. The distro version lags behind and misses security fixes. Run this as root or with sudo:

apt update
apt install -y ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

This installs the Docker engine plus the Compose v2 plugin. Verify both work:

docker --version
docker compose version

Expected output shows a recent Docker version like 27.x and Compose v2.x. If you see docker-compose with a hyphen instead, you have the old standalone binary. The plugin approach with a space is what modern guides and the official docs use.

Step 2 - Prepare the directory structure and environment file

Create a folder for the project. Keeping everything under /opt/n8n is a clean convention for self-hosted services:

mkdir -p /opt/n8n
cd /opt/n8n

Now create the environment file. This is where secrets and configuration live, and where most people get things wrong. The critical variable is WEBHOOK_URL. n8n uses it to build absolute URLs for webhook triggers. If you leave it at the default, n8n will tell external services to call your IP on port 5678, and everything fails once HTTPS is in front.

nano .env

Paste this, replacing the domain and passwords:

N8N_HOST=n8n.yourdomain.com
N8N_PORT=5678
N8N_PROTOCOL=https
WEBHOOK_URL=https://n8n.yourdomain.com
N8N_DATABASE_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n
DB_POSTGRESDB_PASSWORD=change-me-strong-password
N8N_ENCRYPTION_KEY=generate-a-long-random-string
N8N_USER_MANAGEMENT_JWT_SECRET=another-long-random-string
GENERIC_TIMEZONE=Asia/Ho_Chi_Minh

Generate the two random strings with openssl so they are actually strong:

openssl rand -hex 24

Run it twice, once for the encryption key and once for the JWT secret. The encryption key protects your stored credentials. Lose it and the saved passwords in n8n become unrecoverable garbage, so write it down somewhere safe.

Step 3 - Write the docker-compose file

This is the heart of the setup. The file declares two services: postgres for storage and n8n itself. The n8n container connects to Postgres over the internal Docker network, and only port 5678 is exposed to the host, where Nginx will pick it up.

nano docker-compose.yml
services:
  postgres:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: n8n
      POSTGRES_PASSWORD: ${DB_POSTGRESDB_PASSWORD}
      POSTGRES_DB: n8n
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U n8n"]
      interval: 5s
      timeout: 5s
      retries: 10

  n8n:
    image: docker.n8n.io/n8nio/n8n
    restart: unless-stopped
    ports:
      - "5678:5678"
    environment:
      - N8N_HOST=${N8N_HOST}
      - N8N_PORT=${N8N_PORT}
      - N8N_PROTOCOL=${N8N_PROTOCOL}
      - WEBHOOK_URL=${WEBHOOK_URL}
      - N8N_DATABASE_TYPE=${N8N_DATABASE_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}
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - N8N_USER_MANAGEMENT_JWT_SECRET=${N8N_USER_MANAGEMENT_JWT_SECRET}
      - GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  n8n_data:
  postgres_data:

Three details deserve attention. First, the healthcheck on Postgres. Without it, n8n may start before the database is ready and crash in a loop. Second, both volumes are named so they are easy to back up. Third, restart: unless-stopped brings both containers back after a reboot, which matters on a VPS without a console in front of you.

Pull the images and start everything:

docker compose up -d

Watch the startup:

docker compose ps
docker compose logs -f n8n

Within a minute you should see n8n log lines about the editor being ready. If n8n keeps restarting, check the Postgres logs first:

docker compose logs postgres

Authentication failures against Postgres almost always mean a mismatch between .env and what the postgres service was initialized with on the first run. If that happens, run docker compose down -v to wipe the volumes and start again with corrected values. Yes, -v deletes the data, but at this point there is nothing worth keeping.

Step 4 - Set up Nginx as a reverse proxy with HTTPS

n8n is now running on port 5678. Before exposing it, put Nginx in front. It handles TLS termination, HTTP/2, and later you can add rate limiting or gzip without touching the container. Install Nginx and Certbot:

apt install -y nginx certbot python3-certbot-nginx

Create the site configuration:

nano /etc/nginx/sites-available/n8n
server {
    listen 80;
    server_name n8n.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1: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;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        client_max_body_size 50m;
    }
}

The Upgrade and Connection headers matter. n8n uses WebSockets for real-time updates in the editor. Forget those two lines and the editor will connect, then silently disconnect every few seconds. Enable the site and test the config:

ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx

Now request the SSL certificate. Certbot reads the Nginx config, obtains the cert and wires up the HTTPS redirect and renewal in one command:

certbot --nginx -d n8n.yourdomain.com

Follow the prompts. When it finishes, test the HTTPS endpoint:

curl -I https://n8n.yourdomain.com

Expected output is HTTP/2 200 or 302, not a certificate error. If you see a redirect to the setup page, that is correct and expected on first run.

For servers in Vietnam, remember that Let's Encrypt validation reaches the ACME servers over international transit, which is a shared pool of roughly 4 to 10 Mbps. The request may take a few extra seconds, but it works fine. The result is a free SSL certificate that renews automatically, no manual intervention needed.

Step 5 - Complete the n8n setup and verify webhooks

Open https://n8n.yourdomain.com in a browser. The first-run wizard asks you to create an owner account. Do not skip this. n8n now requires an owner account and without N8N_USER_MANAGEMENT_JWT_SECRET set correctly, the login flow fails. If you see an error about JWT, go back to .env, set the secret, and run docker compose up -d again.

After the account is created, the real test: does a webhook trigger work from outside? Create a new workflow, add a Webhook trigger node, set the path to test-webhook, and activate it. Then from your local machine:

curl -X POST https://n8n.yourdomain.com/webhook/test-webhook -H "Content-Type: application/json" -d '{"hello":"world"}'

You should get a 200 response and the execution appears in the n8n editor. If curl resolves to an IP or fails with a hostname error, the A record for your domain is wrong. Fix DNS and wait. If it fails with a TLS error, Certbot did not complete. Run certbot certificates to confirm.

Step 6 - Back up workflows and credentials

Running n8n without a backup plan is how people lose a year of automation logic. The data lives in two places: the Postgres database (workflows, credentials, executions) and the .env file (the encryption key). You need both. The encryption key is the single most important secret on this server, because without it, the credential data in Postgres is undecryptable.

Write a backup script that dumps the database and copies the encrypted volumes. The official, simplest method is docker compose exec with pg_dump:

mkdir -p /opt/n8n-backups
cd /opt/n8n
docker compose exec -T postgres pg_dump -U n8n n8n | gzip > /opt/n8n-backups/n8n-$(date +%F).sql.gz
cp .env /opt/n8n-backups/.env.backup

Restoring is the reverse. Stop n8n, drop the old database, load the dump, start again:

cd /opt/n8n
docker compose stop n8n
gunzip -c /opt/n8n-backups/n8n-2026-06-01.sql.gz | docker compose exec -T postgres psql -U n8n -d n8n
docker compose start n8n

Schedule the backup daily with cron. This line runs it at 3 AM:

crontab -e
0 3 * * * cd /opt/n8n && docker compose exec -T postgres pg_dump -U n8n n8n | gzip > /opt/n8n-backups/n8n-$(date +\%F).sql.gz && cp .env /opt/n8n-backups/.env.backup

Yes, the % character in crontab needs an escaping backslash. It is the number one cron mistake with date commands. If you leave it out, the job will fail or write an empty file, depending on the shell.

For a more complete disaster-recovery story, deploy this on a provider that offers snapshots, so an entire failed upgrade is one rollback click away, not a database restore.

Common problems and how to fix them

Every self-hosted n8n setup hits at least one of these. Here is what to check first.

n8n keeps restarting in a loop

Check the n8n logs first: docker compose logs n8n | tail -50. The usual cause is a Postgres connection failure. Verify Postgres is healthy: docker compose ps. If the postgres container shows unhealthy, check its logs for authentication errors. The database user and password only apply on the very first initialization of the volume. Change them later and you must recreate the volume with docker compose down -v.

The editor loads but executions fail with connection errors

This is almost always the missing WebSocket headers in Nginx. Confirm proxy_set_header Upgrade and Connection "upgrade" are present in your config, then reload Nginx. The editor uses WebSockets for live updates, and without those headers it falls back to polling, which breaks long-running executions in the UI.

Webhook calls fail from external services

The service calling your webhook resolves the hostname and gets your IP, then connects to port 443. If you did not configure WEBHOOK_URL, n8n may answer with a URL pointing at localhost:5678. Set WEBHOOK_URL=https://n8n.yourdomain.com in .env and restart n8n. Also confirm the A record points at the correct public IP, a common mistake when you have multiple VPS instances.

FAQ

What are the minimum resources to run n8n with Docker?

Docker itself plus Postgres and n8n run comfortably on a 2GB RAM VPS with 2 vCPUs. For active automation with many concurrent executions and webhooks, go up to 4GB RAM. n8n is a Node.js app, and Postgres buffers its own cache, so RAM is what you feel first.

Should I use SQLite or Postgres for n8n?

Use Postgres for anything beyond personal testing. SQLite works but locks the whole database during writes, which becomes a visible bottleneck once multiple workflows execute at the same time. Postgres handles concurrency properly and the docker-compose file above sets it up in minutes.

How do I update n8n to a new version?

Pull the new image and recreate the containers: docker compose pull n8n then docker compose up -d n8n. The Postgres container stays untouched. Check the n8n changelog before major version upgrades, as some require a database migration step that runs automatically on first start.

Is it safe to expose port 5678 publicly?

No. Keep port 5678 bound to the internal Docker network or localhost only. The editor and API should be reached through Nginx on 443. If your firewall is open, Nginx handles TLS, rate limiting, and access control, which n8n's raw HTTP server does not.

What happens if I lose the N8N_ENCRYPTION_KEY?

All saved credentials in n8n become permanently undecryptable. Workflows stay visible, but every node that uses a stored credential will fail. This key must be stored off-server, in your password manager or a secure note, right after setup.

Related articles

用 Docker 在 VPS 上部署 n8n 的核心要点

这篇指南讲解了在 Ubuntu 24.04 VPS 上用 docker-compose 部署 n8n 的完整流程。关键是使用 Postgres 作为数据库,通过 Nginx 反向代理配置 HTTPS,并正确设置 WEBHOOK_URL 环境变量,否则外部服务无法调用你的 Webhook。备份时必须同时保存 Postgres 数据库和 N8N_ENCRYPTION_KEY 加密密钥,缺少密钥则所有已保存的凭据都无法解密。对于越南用户,选择本地机房提供的越南 IPv4 可以降低国内访问延迟,建议从提供 KVM 虚拟化和 NVMe 硬盘的合格服务商租用 VPS。

Note: This guide is for general reference. Every system and infrastructure has its own specifics, so test each step in a safe environment and consult a qualified engineer before applying it in production.