Tự host n8n trên VPS: deploy Docker Compose với Postgres, HTTPS và backup
n8n là một trong những công cụ tự động hoá workflow theo hướng fair-code phổ biến nhất hiện nay: bạn nối các node (trigger, HTTP request, database, LLM, if/switch) thành luồng chạy tự động, thay cho việc viết script rời rạc. Bản cloud của n8n tính phí theo số lần thực thi (execution), nên khi khối lượng workflow tăng, chi phí leo thang nhanh và khó dự đoán. Self-host n8n trên một VPS bạn kiểm soát giúp bỏ trần execution đó, giữ dữ liệu credential trong hạ tầng của mình, và tuỳ biến biến môi trường theo ý muốn. Bài này đi thẳng vào một bản deploy production bằng Docker Compose kèm Postgres, cấu hình WEBHOOK_URL sau reverse proxy để có HTTPS, persistence, vài lớp bảo mật cơ bản và backup. Toàn bộ tham chiếu theo tài liệu self-hosting chính thức của n8n, chuẩn 2026.
Vì sao self-host thay vì dùng cloud
Điểm khác biệt lớn nhất nằm ở mô hình chi phí. Bản cloud tính theo execution, mỗi lần workflow chạy là một đơn vị bị đếm, kể cả các luồng polling hay retry chạy nền. Khi bạn dựng vài chục workflow đồng bộ dữ liệu chạy mỗi phút, con số execution tháng tăng theo cấp số nhân. Self-host trên VPS chuyển chi phí thành một khoản cố định theo tài nguyên máy chủ, không phụ thuộc số lần chạy.
Ngoài chi phí, self-host còn cho bạn:
- Kiểm soát dữ liệu: credential (API key, token, mật khẩu DB) nằm trong Postgres của bạn, mã hoá bằng key bạn giữ, không rời khỏi hạ tầng.
- Toàn quyền cấu hình: tự đặt biến môi trường, mount volume, chạy custom node, kết nối tới dịch vụ nội bộ trong cùng mạng riêng.
- Không giới hạn nhân tạo: số workflow, số execution chỉ bị chặn bởi CPU/RAM thật của VPS.
Đổi lại, bạn nhận trách nhiệm vận hành: cập nhật phiên bản, backup, giám sát và vá bảo mật. Phần còn lại của bài giúp bạn dựng nền tảng đó cho gọn.
Tại sao Postgres, không dùng SQLite cho production
Mặc định n8n dùng SQLite để chạy thử cho nhanh. Với production, đây là lựa chọn sai. SQLite khoá ghi ở mức file, nên khi nhiều workflow ghi execution log đồng thời sẽ xuất hiện nghẽn và lỗi database is locked. File SQLite cũng dễ phình to và khó backup nóng khi n8n đang chạy.
Postgres xử lý ghi đồng thời tốt hơn hẳn, hỗ trợ backup nhất quán bằng pg_dump ngay khi service còn sống, và là hướng n8n khuyến nghị cho mọi triển khai nghiêm túc. Biến quyết định database engine là DB_TYPE=postgresdb. Thiếu biến này, n8n âm thầm quay về SQLite dù bạn đã khai báo host Postgres.
compose.yaml với n8n và Postgres
Cấu trúc gồm hai service trong cùng một Docker network: postgres lưu dữ liệu và n8n chạy ứng dụng. Mỗi service có một named volume để dữ liệu sống sót qua mỗi lần restart hay nâng cấp container.
services:
postgres:
image: postgres:16
restart: unless-stopped
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=doi_mat_khau_manh_o_day
- POSTGRES_DB=n8n
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U n8n -d n8n']
interval: 10s
timeout: 5s
retries: 5
n8n:
image: docker.n8n.io/n8nio/n8n:latest
restart: unless-stopped
ports:
- '127.0.0.1:5678:5678'
depends_on:
postgres:
condition: service_healthy
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=doi_mat_khau_manh_o_day
- N8N_HOST=n8n.example.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://n8n.example.com/
- N8N_EDITOR_BASE_URL=https://n8n.example.com/
- N8N_ENCRYPTION_KEY=chuoi_ngau_nhien_32_ky_tu_giu_ky
- GENERIC_TIMEZONE=Asia/Ho_Chi_Minh
- TZ=Asia/Ho_Chi_Minh
- N8N_RUNNERS_ENABLED=true
- N8N_PROXY_HOPS=1
volumes:
- n8n_data:/home/node/.n8n
volumes:
postgres_data:
n8n_data:
Vài điểm cần chú ý trong file trên:
DB_POSTGRESDB_HOST=postgrestrỏ tới tên service, không phảilocalhost. n8n chạy trong container riêng nên phải gọi Postgres qua Docker network bằng tên service, dùnglocalhostsẽ báo connection refused.ports: '127.0.0.1:5678:5678'chỉ mở port ra loopback của host, không phơi ra Internet. Reverse proxy (chạy trên cùng host hoặc container khác) sẽ là cửa duy nhất tiếp cận n8n.N8N_ENCRYPTION_KEYlà key mã hoá credential. Đặt cố định và backup riêng: mất key này thì mọi credential đã lưu không giải mã lại được. Nếu bỏ trống, n8n tự sinh và ghi vào volume, nhưng khai báo tường minh giúp bạn chủ động khi khôi phục.N8N_RUNNERS_ENABLED=truebật task runners, cơ chế thực thi code node tách tiến trình được n8n khuyến nghị cho triển khai mới trong 2026.N8N_PROXY_HOPS=1báo cho n8n biết nó đứng sau một reverse proxy, để xử lý đúng headerX-Forwarded-For.
Sinh encryption key và khởi động:
openssl rand -hex 16
docker compose up -d
docker compose logs -f n8n
WEBHOOK_URL và reverse proxy cho HTTPS
n8n dùng WEBHOOK_URL để dựng đường dẫn webhook công khai mà nó đưa cho dịch vụ bên ngoài (ví dụ webhook của Stripe, GitHub, Telegram). Nếu để trống, n8n suy ra URL từ host nội bộ và sinh ra đường dẫn dạng http://localhost:5678/..., khiến webhook bên ngoài không gọi tới được. Vì vậy phải đặt WEBHOOK_URL đúng bằng URL HTTPS công khai của bạn, kết thúc bằng dấu /.
Quan trọng: TLS được kết thúc ở reverse proxy, không phải ở container n8n. Proxy nhận HTTPS trên port 443, giải mã, rồi chuyển tiếp HTTP tới 127.0.0.1:5678. Đừng bao giờ phơi port 5678 trực tiếp ra Internet. Caddy là lựa chọn gọn vì tự xin và gia hạn chứng chỉ Let's Encrypt:
n8n.example.com {
reverse_proxy 127.0.0.1:5678
}
Với Nginx, cần chuyển tiếp thêm header nâng cấp WebSocket vì giao diện editor của n8n dùng push connection:
server {
listen 443 ssl;
server_name n8n.example.com;
ssl_certificate /etc/letsencrypt/live/n8n.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/n8n.example.com/privkey.pem;
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-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Sau khi đổi WEBHOOK_URL hoặc host, chạy lại docker compose up -d để n8n nạp biến mới, rồi mở workflow có webhook và kiểm tra URL sinh ra đã đúng domain HTTPS.
Persistence: giữ dữ liệu qua mỗi lần nâng cấp
Hai named volume trong compose là ranh giới giữa nâng cấp an toàn và mất trắng. postgres_data giữ toàn bộ workflow, credential đã mã hoá và lịch sử execution. n8n_data (mount vào /home/node/.n8n) giữ file cấu hình và encryption key do n8n sinh. Khi nâng cấp, bạn chỉ kéo image mới rồi dựng lại container, dữ liệu trong volume không bị đụng tới:
docker compose pull
docker compose up -d
Vì volume tách rời container, việc docker compose down rồi up lại không làm mất gì, miễn bạn không thêm cờ -v (cờ đó xoá cả volume). Đừng lưu dữ liệu quan trọng trong lớp ghi của container, luôn để trong volume.
Bảo mật cơ bản
Một n8n phơi ra Internet là mục tiêu ngon vì nó nắm credential của nhiều dịch vụ khác. Vài lớp tối thiểu:
- Firewall: chỉ mở port 80 và 443 ra ngoài, chặn 5678. Với UFW:
ufw allow 80,443/tcpvà không mở 5678. - HTTPS bắt buộc: để reverse proxy ép TLS. n8n mặc định bật secure cookie, tức là session chỉ gửi qua HTTPS, nên bản thân cấu hình đã đòi bạn chạy qua proxy có chứng chỉ hợp lệ.
- Bảo vệ encryption key: backup
N8N_ENCRYPTION_KEYở nơi riêng biệt (password manager), tách khỏi bản backup database. - Cập nhật đều: theo dõi release của n8n và kéo image mới định kỳ để vá lỗ hổng.
- Mật khẩu mạnh cho Postgres: dù DB không phơi ra ngoài, vẫn đặt mật khẩu mạnh và không tái sử dụng.
Với môi trường nhiều người dùng, dùng tính năng user management sẵn có của n8n để tạo tài khoản riêng thay vì chia sẻ một đăng nhập.
Backup và khôi phục
Cần backup hai thứ, thiếu một là khôi phục thất bại: database Postgres và encryption key. Dump database khi n8n vẫn đang chạy:
docker compose exec -T postgres pg_dump -U n8n n8n | gzip > n8n-backup-$(date +%F).sql.gz
Đưa lệnh này vào cron chạy hằng ngày và đẩy file backup ra ngoài VPS (object storage hoặc máy khác). Encryption key thì backup một lần và giữ cố định; workflow khôi phục từ database chỉ giải mã lại credential được khi key khớp. Để phục hồi trên máy mới:
gunzip -c n8n-backup-2026-07-15.sql.gz | docker compose exec -T postgres psql -U n8n n8n
Sau khi restore, đảm bảo N8N_ENCRYPTION_KEY trên máy mới đúng bằng key cũ trước khi khởi động n8n, nếu không credential sẽ hiện nhưng không dùng được.
Tóm lược
Self-host n8n trên VPS là một bài toán vận hành gọn nếu bạn tách bạch đúng các phần: Postgres thay SQLite cho production, hai named volume để persistence sống qua nâng cấp, reverse proxy kết thúc HTTPS còn container chỉ nghe loopback, và WEBHOOK_URL đặt bằng đúng URL công khai để webhook bên ngoài gọi tới được. Ba biến hay gây lỗi nhất là DB_TYPE=postgresdb (thiếu thì quay về SQLite), DB_POSTGRESDB_HOST=postgres (dùng tên service, không phải localhost), và N8N_ENCRYPTION_KEY (mất là mất credential). Khi đã dựng xong, thứ còn lại chỉ là kỷ luật: backup database và key hằng ngày, kéo image mới định kỳ, và giữ firewall chặt. Chi tiết từng biến môi trường luôn nên đối chiếu lại với tài liệu environment variables của n8n.