Bỏ qua đến nội dung chính
AI Automation
Logo n8n và URL webhook HTTPS trên tên miền riêng

Tự host n8n bằng Docker trên VPS từ A đến Z

Nguyễn Việt Chung

Đăng 12 phút đọc

Mục lục · 20

Hướng dẫn dựng n8n 2.42.5 trên hai máy thật, một máy có IP public và một máy không, kèm cấu hình VPS nào chạy được, cách khóa lỗ hổng của bản cài mặc định, so sánh Caddy, frp và Cloudflare Tunnel, và chuỗi lỗi khi chuyển n8n sang máy mới.

Tự host n8n là chạy n8n trên máy chủ của bạn bằng Docker thay vì thuê bản n8n Cloud: dữ liệu và credential nằm trên máy bạn, không giới hạn số workflow, chỉ tốn tiền máy chủ. Đổi lại, bạn tự lo bảo mật, HTTPS, sao lưu và cập nhật.

Những điều cần nắm trước khi đọc tiếp:

  • Lệnh cài chính thức curl -fsSL https://get.n8n.io | sh chạy xong trong 63 đến 69 giây, nhưng mở cổng 5678 cho cả Internet: trên máy có IP public, người lạ vào được trang tạo tài khoản quản trị.

  • VPS 1 CPU, 1GB RAM vẫn chạy được, xử lý 10 webhook mỗi giây; 2 CPU lên 18.

  • Có IP public: Caddy cấp HTTPS tự động sau 23 giây. Không có IP tĩnh: frp qua một VPS rẻ chậm thêm khoảng 5ms, Cloudflare quick tunnel chậm hơn gần 3 lần và đổi địa chỉ mỗi lần khởi động lại.

  • Chuyển máy thì chép nguyên volume dữ liệu (1,6MB, khôi phục 29 giây). Export rồi import sang máy mới làm credential không giải mã được.

Mình làm tất cả trên hai máy Ubuntu 24.04 thuê ở AIDev Cloud ngày 08 và 09/10/2026, n8n 2.42.5, Docker 29.6.1. Nếu bạn chưa biết n8n làm được gì, đọc bài n8n là gì trước.

Tự host n8n cần VPS cấu hình thế nào?

Mình chạy n8n trong container giới hạn CPU và RAM bằng docker --cpus và --memory để giả lập VPS giá rẻ, rồi bắn 1.000 webhook với 20 luồng cùng lúc:

Biểu đồ số webhook n8n xử lý mỗi giây theo cấu hình: 1 CPU 1GB được 10, 2 CPU 2GB được 18, 25 nhân SQLite 37, Postgres 46

Cấu hình

Request/giây

Trễ p95

Ghi chú

1 CPU, 1GB RAM

10

2,5 giây

RAM cao nhất 626MB, không bị kill

2 CPU, 2GB RAM

18

1,3 giây

RAM cao nhất vẫn 626MB

25 nhân, SQLite

37

0,6 giây

bộ cài get.n8n.io đủ 5 container

25 nhân, Postgres

46

0,48 giây

Postgres dùng thêm 58MB RAM

Workflow dùng để đo gồm Webhook, Code tính tiền và Respond to Webhook. Kết luận của mình: n8n cần thêm CPU hơn là thêm RAM. Bộ cài một dòng có thêm sandbox cho n8n Assistant, tài liệu chính thức ghi cần ít nhất 4GB RAM và 2 vCPU cho bộ này; chỉ cần n8n thì 1GB vẫn chạy.

Ổ đĩa: bộ image chiếm 3,68GB. n8n tự dọn lịch sử chạy sau 14 ngày hoặc khi vượt 10.000 lần chạy (EXECUTIONS_DATA_MAX_AGE=336, EXECUTIONS_DATA_PRUNE_MAX_COUNT=10000). Trên máy mình, khoảng 1.200 lần chạy chiếm 22MB cơ sở dữ liệu.

Cài n8n bằng Docker trên VPS như thế nào?

Từ n8n 3.0 (dự kiến tháng 10/2026), n8n chỉ phát hành qua Docker, npm install n8n không còn dùng cho bản mới. Cách cài được tài liệu khuyến nghị hiện nay là một dòng lệnh, chạy trên máy đã có Docker và Docker Compose v2:

bash
docker compose version
curl -fsSL https://get.n8n.io | sh

Script tạo thư mục n8n/ gồm compose.yml, .env (mật khẩu nội bộ sinh ngẫu nhiên) và searxng-settings.yml, rồi kéo 5 container. Trên máy có IP public mình đo 63 giây, máy còn lại 69 giây:

docker compose ps sau khi cài n8n: 5 container n8n, runners, sandbox-api, sandbox-runner, searxng và RAM từng container

Các lệnh dùng hằng ngày, chạy trong thư mục chứa n8n/:

bash
docker compose -f ./n8n/compose.yml down      # dừng
docker compose -f ./n8n/compose.yml up -d     # chạy lại
docker compose -f ./n8n/compose.yml logs n8n --tail 50

Vì sao phải khóa cổng 5678 ngay sau khi cài?

File compose.yml mặc định có dòng - '5678:5678', nghĩa là n8n nghe trên mọi địa chỉ của máy. Trên máy có IP public, ngay sau khi cài, mình đứng từ một máy khác gọi vào và thấy showSetupOnFirstLoad = True: trang tạo tài khoản owner đang mở cho bất kỳ ai biết IP.

Sửa bằng cách chỉ cho n8n nghe ở 127.0.0.1, mọi truy cập từ ngoài đi qua reverse proxy có HTTPS ở bước sau:

bash
cd n8n
sed -i "s#- '5678:5678'#- '127.0.0.1:5678:5678'#" compose.yml
docker compose up -d n8n
Trước khi sửa, n8n trả showSetupOnFirstLoad True từ Internet; sau khi đổi sang 127.0.0.1, curl báo Failed to connect

Làm bước này trước cả khi tạo tài khoản. Cũng đừng mở cổng của container sandbox-runner, vì nó chạy Docker-in-Docker với quyền privileged; script cài có in sẵn cảnh báo này.

Gắn tên miền và HTTPS cho n8n bằng Caddy

Mình dùng Caddy (77.533 sao trên GitHub) vì nó tự xin và gia hạn chứng chỉ Let's Encrypt. Trỏ bản ghi A của tên miền về IP máy chủ, rồi tạo n8n/Caddyfile:

text
n8n-lab.forai.dev {
	reverse_proxy n8n:5678
}

Thêm service caddy vào cuối khối services: trong compose.yml, và thêm caddy-data: vào khối volumes: ở đầu file:

yaml
  caddy:
    image: caddy:2.11.7
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
    depends_on:
      - n8n

Lần đầu mình chèn nhầm đoạn này vào khối volumes: (file của n8n đặt volumes lên trước services), Compose báo lỗi:

text
validating /root/n8n/compose.yml:  additional properties 'caddy' not allowed

Cuối cùng báo cho n8n biết nó đang chạy sau proxy, thêm vào .env:

bash
N8N_WEBHOOK_URL=https://n8n-lab.forai.dev/
N8N_EDITOR_BASE_URL=https://n8n-lab.forai.dev/
N8N_PROXY_HOPS=1

Nếu bạn từng đặt biến WEBHOOK_URL thì lưu ý: theo tài liệu reverse proxy của n8n, từ bản 2.35.0 biến đúng là N8N_WEBHOOK_URL, biến cũ chỉ còn được hỗ trợ kèm cảnh báo. Thiếu biến này, URL webhook trong editor vẫn là localhost, dịch vụ bên ngoài không gọi vào được:

Node Webhook trong n8n chưa đặt địa chỉ: Test URL là http://localhost:5678/webhook-test/don-hang

Chạy docker compose up -d, Caddy lấy chứng chỉ trong 23 giây; http:// tự chuyển sang https:// (mã 308), chứng chỉ Let's Encrypt có hạn 90 ngày và Caddy tự gia hạn. Mở lại node Webhook, URL đã thành tên miền:

Node Webhook sau khi đặt N8N_WEBHOOK_URL: Test URL là https://n8n-lab.forai.dev/webhook-test/don-hang

Gọi webhook từ một máy ở Hà Nội qua tên miền: lần đầu 2,09 giây, các lần sau 62 đến 67 mili giây.

Không có IP tĩnh thì có làm HTTPS cho n8n được không?

Được. Mình để n8n trên một máy không có IP public (giống máy ở nhà hay ở công ty sau NAT) rồi thử hai cách đưa nó ra Internet, đo độ trễ webhook từ Hà Nội:

Biểu đồ độ trễ gọi webhook n8n từ Internet: IP public và Caddy 65ms, frp qua VPS 70ms, Cloudflare quick tunnel 187ms

Cách

Cần gì

Trễ trung vị

Nhược điểm mình gặp

IP public + Caddy

VPS có IP

65ms

Máy n8n phải có IP public

frp qua VPS cầu nối

1 VPS rẻ có IP + tên miền

70ms

Phải giữ thêm một máy và token

Cloudflare quick tunnel

Không cần gì

187ms

Đi vòng Hồng Kông, đổi URL mỗi lần khởi động

Cách 1: frp qua một VPS rẻ

frp (109.781 sao) mở một đường hầm từ máy ở nhà ra VPS. Trên VPS, chạy frps cạnh Caddy trong cùng compose.yml và cho Caddy chuyển cổng 8443 vào đường hầm.

File n8n/frp/frps.toml trên VPS:

text
bindPort = 7000
auth.token = "chuỗi bí mật dài"

Thêm vào Caddyfile, và thêm service frps (image alpine chạy file frps, mở cổng 7000) vào compose.yml:

text
n8n-lab.forai.dev:8443 {
	reverse_proxy frps:6000
}

Trên máy ở nhà, tải frpc v0.71.0 từ trang release và tạo /etc/frp/frpc.toml:

text
serverAddr = "IP_CUA_VPS"
serverPort = 7000
auth.token = "chuỗi bí mật dài"

[[proxies]]
name = "n8n-nha"
type = "tcp"
localIP = "127.0.0.1"
localPort = 5678
remotePort = 6000

Đặt N8N_WEBHOOK_URL=https://n8n-lab.forai.dev:8443/ ở máy nhà rồi khởi động lại n8n. Cổng 6000 chỉ nằm trong mạng Docker trên VPS, người ngoài không gọi thẳng vào được:

frpc báo start proxy success, webhook gọi qua https://n8n-lab.forai.dev:8443 trả kết quả trong khoảng 0,07 giây

Cách 2: Cloudflare quick tunnel

Không cần VPS hay tài khoản: cài cloudflared 2026.10.0 rồi chạy cloudflared tunnel --url http://127.0.0.1:5678, sau 4 giây có ngay một địa chỉ https://….trycloudflare.com. Máy của mình được nối qua máy chủ Cloudflare ở Hồng Kông nên mỗi lần gọi mất khoảng 187ms.

cloudflared tạo quick tunnel trycloudflare, webhook chạy được; khởi động lại thì URL đổi và URL cũ trả HTTP 530

Vấn đề là khởi động lại cloudflared thì địa chỉ đổi, địa chỉ cũ trả lỗi 530, mọi webhook đã đăng ký với dịch vụ khác hỏng theo. Chính cloudflared cũng ghi tunnel không tài khoản "no uptime guarantee". Muốn địa chỉ cố định phải tạo named tunnel gắn tên miền trong tài khoản Cloudflare; mình chưa làm phần đó nên không có số đo.

Nên dùng SQLite hay Postgres cho n8n?

Bộ cài một dòng dùng SQLite, đủ cho một người hoặc nhóm nhỏ. Khi đổi sang Postgres 18 theo hướng dẫn chính thức, máy mình xử lý 46 webhook/giây thay vì 37, trễ trung vị 48ms thay vì 69ms.

Chỗ cần cẩn thận: đổi DB_TYPE sang postgresdb là n8n khởi động với một cơ sở dữ liệu trống. Nó chạy 286 migration, tạo 148 bảng, 0 workflow, và trang tạo tài khoản owner mở lại. Máy của mình lúc đó đang ra Internet qua frp nên mình phải vào tạo lại tài khoản ngay. Tài liệu có ghi dữ liệu SQLite không tự chuyển sang, nhưng đọc lướt rất dễ bỏ qua.

Thứ tự mình làm để chuyển dữ liệu, mất 17 giây. Trước khi đổi DB, export ra một thư mục nằm trong volume:

bash
docker compose exec n8n mkdir -p /home/node/.n8n/truoc-pg
docker compose exec n8n n8n export:workflow --all --output=/home/node/.n8n/truoc-pg/wf.json
docker compose exec n8n n8n export:credentials --all --output=/home/node/.n8n/truoc-pg/cred.json

Sau đó thêm service postgres và các biến DB_* theo tài liệu, chạy docker compose up -d, tạo lại tài khoản owner. Cuối cùng import và bật lại workflow:

bash
docker compose exec n8n n8n import:workflow --input=/home/node/.n8n/truoc-pg/wf.json
docker compose exec n8n n8n import:credentials --input=/home/node/.n8n/truoc-pg/cred.json
docker compose exec n8n n8n publish:workflow --id=ID_WORKFLOW
docker compose restart n8n

Import tự tắt mọi workflow ("Deactivating workflow…"), và lệnh publish:workflow báo phải khởi động lại n8n mới có hiệu lực. Với Postgres 18 phải giữ dòng PGDATA: /var/lib/postgresql/data trong service postgres, nếu không Postgres ghi dữ liệu ra ngoài volume.

Sao lưu và chuyển n8n sang máy khác thế nào?

Credential trong n8n được mã hóa bằng một khóa sinh lúc chạy lần đầu, lưu ở file /home/node/.n8n/config trong volume. File export credential cũng là dữ liệu đã mã hóa. Mình thử chuyển sang máy mới bằng export rồi import, kết quả là một chuỗi lỗi:

Chuyển n8n sang máy mới: import báo thành công nhưng credential không giải mã được, đặt khóa cũ thì Mismatching encryption keys, thay file config thì lỗi signing.hmac

Import báo thành công nhưng credential không đọc được. Đặt N8N_ENCRYPTION_KEY cũ vào .env thì n8n không khởi động vì khóa trong file config không khớp. Ghi đè file config bằng tài khoản root thì gặp EACCES: permission denied, vì n8n chạy bằng user uid 1000. Sửa quyền xong vẫn lỗi signing.hmac, vì n8n đã mã hóa khóa nội bộ bằng khóa mới ngay lần chạy đầu.

Cách chạy được ngay: chép nguyên volume. Volume của mình nén còn 1,6MB, dừng và đóng gói mất 3 giây.

Trên máy cũ:

bash
docker compose stop n8n
docker run --rm -v n8n_n8n-data:/data:ro -v /root:/out alpine tar czf /out/n8n-data.tgz -C /data .
docker compose start n8n

Chép file n8n-data.tgz sang máy mới đã cài bằng get.n8n.io, rồi:

bash
docker compose stop n8n && docker compose rm -f n8n
docker volume rm n8n_n8n-data && docker volume create n8n_n8n-data
docker run --rm -v n8n_n8n-data:/data -v /root:/in alpine tar xzf /in/n8n-data.tgz -C /data
docker compose up -d n8n

Khôi phục trên máy mới mất 29 giây, đủ 3 workflow, credential giải mã được. Nếu vẫn muốn export/import, đặt N8N_ENCRYPTION_KEY bằng khóa cũ trước khi n8n trên máy mới chạy lần đầu. Lệnh export cũng không tự tạo thư mục, gõ sai đường dẫn sẽ gặp ENOENT: no such file or directory.

Cập nhật n8n lên bản mới thế nào?

Với bộ cài một dòng, chạy lại script kèm --upgrade ở thư mục cha của n8n/. Mình chạy nhầm bên trong thư mục n8n/ và nhận:

text
Error: no existing install found in ./n8n — run without --upgrade to install.

Chạy đúng chỗ thì script chỉ sửa hai dòng N8N_VERSION và N8N_SANDBOX_VERSION trong .env; phần extra_hosts và Caddy mình thêm vào compose.yml vẫn còn nguyên. Trước khi lên bản 3.0, mở Settings → Migration report để xem workflow nào dùng node sắp bị xóa, chi tiết trong bài n8n 3.0 có gì mới.

Những lỗi mình gặp khi dựng n8n trên VPS

Lỗi

Nguyên nhân

Cách sửa

Người lạ vào được trang tạo owner

Cổng 5678 nghe mọi địa chỉ

127.0.0.1:5678:5678 + reverse proxy

additional properties 'caddy' not allowed

Chèn service vào khối volumes:

Đặt dưới services:

URL webhook vẫn là localhost

Thiếu N8N_WEBHOOK_URL

Đặt biến, khởi động lại n8n

Credentials could not be decrypted

Import sang máy có khóa khác

Chép nguyên volume

Mismatching encryption keys

Đặt khóa cũ sau khi đã chạy

Đặt trước lần chạy đầu

no existing install found in ./n8n

Chạy --upgrade sai thư mục

Chạy ở thư mục cha

Webhook 404 ngay sau khi khởi động lại

Workflow chưa đăng ký lại xong

Chờ vài giây rồi gọi lại

Hai thông báo lỗi nguyên văn mình gặp khi chuyển máy:

text
Credentials could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.
text
Error: Mismatching encryption keys. The encryption key in the settings file /home/node/.n8n/config does not match the N8N_ENCRYPTION_KEY env var. Please make sure both keys match.

Nếu bạn nối n8n với Ollama chạy trên cùng máy để làm trợ lý AI, còn một lỗi nữa về 127.0.0.1 trong container, cách sửa có trong bài n8n là gì.

Khi nào không nên tự host n8n?

  • Không ai trong nhóm biết SSH, Docker để xử lý khi máy lỗi hoặc cần cập nhật. Khi đó n8n Cloud đỡ rủi ro hơn.

  • Workflow giữ dữ liệu quan trọng mà bạn chưa có lịch sao lưu volume ra máy khác.

  • Bạn định cho khách tự dựng workflow trên n8n của mình. Giấy phép bản miễn phí không cho phép việc này.

Câu hỏi thường gặp

Tự host n8n có mất phí không?

Bản tự host miễn phí theo giấy phép Sustainable Use License, bạn chỉ trả tiền máy chủ. Giới hạn chính: không được bán n8n như một dịch vụ cho người khác tự dựng workflow.

Có cài n8n không cần Docker được không?

Hiện vẫn cài được bằng npm, nhưng tài liệu n8n ghi từ bản 3.0 sẽ không phát hành gói npm chạy được nữa. Cài mới thì nên dùng Docker ngay từ đầu.

Cài n8n trên Windows được không?

Được, qua Docker Desktop với WSL2. Tài liệu khuyên để thư mục n8n trong ổ của WSL (ví dụ ~/n8n), không đặt dưới /mnt/c, vì chép qua lại giữa hai hệ thống file chậm và dễ lỗi quyền.

Dùng ngrok thay cho frp hay Cloudflare được không?

Về nguyên lý giống quick tunnel: máy ở nhà mở đường hầm ra một máy chủ trung gian. Mình chưa đo ngrok nên không có con số để so.

Đọc tiếp

Muốn trợ lý trong n8n chạy model trên GPU riêng mà máy chưa có card, AIDev Cloud cho thuê máy GPU theo giờ; hai máy Tesla P40 dùng trong bài có giá 7.000 đồng/giờ (thêm IP public là 8.000 đồng/giờ) ngày 08/10/2026.

Đo trên hai máy Tesla P40 của AIDev Cloud ngày 08 và 09/10/2026: n8n 2.42.5, Docker 29.6.1, Docker Compose 5.2.0, Caddy 2.11.7, frp 0.71.0, cloudflared 2026.10.0, Postgres 18.6, Ubuntu 24.04.

Chuyên mục

AI Automation

Về tác giả

Nguyễn Việt Chung

Founder AIDev

Thạc sĩ AI MSE, tốt nghiệp thủ khoa với điểm 9,4/10. Đồng sáng lập, CTO E-University, SVOnline: số hóa 10+ đại học, 40.000+ sinh viên. Sáng lập AIDev và VietPlatform, xây hệ thống AI cho doanh nghiệp. Anh trực tiếp biên soạn và giảng dạy toàn bộ lộ trình AI Engineer.