SSD Nodes Learn 🎉 VPS từ $5.50/tháng
Hướng dẫn Matt ConnorBởi Matt Connor

Vì sao n8n liên tục offline trên VPS?

n8n offline có thể do banner websocket, restart loop, bị kernel kill vì thiếu RAM hoặc workflow không chạy. Phân biệt 4 lỗi trước khi sửa cấu hình.

Vì sao n8n liên tục offline: bốn lỗi, một triệu chứng

“n8n liên tục offline” là một câu mô tả bốn lỗi khác nhau. Mỗi lỗi cần một cách xử lý riêng. Trình chỉnh sửa hiển thị banner mất kết nối trong khi container vẫn đang chạy bình thường. Container tự khởi động lại. Kernel kill tiến trình Node.js vì tiến trình này dùng quá nhiều bộ nhớ. Hoặc bản thân tiến trình không có vấn đề gì, nhưng một workflow đang active lại không bao giờ được kích hoạt. Nếu thay đổi sai setting, bạn sẽ mất cả cuối tuần để xử lý một vấn đề vốn không tồn tại.

Vì vậy, hãy xác định bạn đang gặp lỗi nào trước khi thay đổi bất kỳ cấu hình nào. n8n chạy dưới dạng một tiến trình Node.js duy nhất, thường nằm trong một Docker container, phía sau reverse proxy thực hiện TLS termination (transport layer security). Mỗi lớp có thể hỏng theo một cách riêng, nhưng browser lại báo tất cả bằng cùng một thông báo.

Chẩn đoán theo thứ tự này

Chạy các lệnh sau trên VPS (virtual private server) và đọc các giá trị mà chính máy của bạn in ra. Không so sánh chúng với các con số trong một bài đăng trên forum. Những giá trị cần quan tâm mô tả máy của bạn, không phải máy của người khác.

docker ps -a --filter name=n8n
docker logs --tail 200 --timestamps n8n
docker inspect n8n | grep -iE 'Status|Running|RestartCount|OOMKilled|ExitCode'
docker stats --no-stream

Cột STATUS từ docker ps -a cho biết container đã ở trạng thái hiện tại trong bao lâu. So sánh thời gian này với thời điểm sự cố bắt đầu. Nếu container đã chạy từ lâu trước khi banner xuất hiện, thì n8n chưa từng offline. Vấn đề nằm ở kết nối giữa browser và backend, cụ thể là đường websocket được đề cập trong phần tiếp theo.

RestartCount là số lần Docker đã restart container này. Ghi lại con số đó, chờ một phút rồi đọc lại. Nếu con số tăng trong lúc bạn theo dõi, container đang trong vòng lặp restart. Các dòng log ngay trước mỗi lần restart sẽ cho biết nguyên nhân.

OOMKilled là một cờ true hoặc false. True nghĩa là Linux kernel đã kill process vì process vượt quá memory limit, có thể là limit riêng của container hoặc của toàn bộ máy. Trường này giúp phân biệt một lần bị kill vì thiếu memory với mọi kiểu exit khác. Vì vậy, hãy đọc trường này trước khi phỏng đoán.

ExitCode là mã mà container sử dụng trong lần exit gần nhất. Bạn không cần ghi nhớ ý nghĩa của từng mã. Hãy đọc mã của mình, sau đó đọc phần cuối của docker logs tại cùng timestamp. Phần cuối log và cờ out of memory khi xem cùng nhau sẽ cho biết chuyện gì đã xảy ra; nếu chỉ xem một trong hai thì có thể kết luận sai.

docker stats hiển thị memory đang sử dụng cùng với limit hiện tại. Để lệnh này chạy trong một terminal thứ hai, kích hoạt workflow gây lỗi rồi theo dõi giá trị thay đổi như thế nào trong lúc sự cố xảy ra.


Trình biên tập n8n duy trì một kết nối push lâu dài đến backend để truyền tiến độ thực thi lên canvas. Mặc định, kết nối này là WebSocket và được chọn bằng N8N_PUSH_BACKEND, với giá trị mặc định là websocket. WebSocket bắt đầu bằng một HTTP request thông thường chứa các header Connection: UpgradeUpgrade: websocket. Server trả về 101 Switching Protocols, sau đó hai bên sử dụng cùng một TCP socket để truyền dữ liệu theo cả hai chiều.

Có hai nguyên nhân làm hỏng kết nối này, và cả hai đều nằm ở proxy chứ không phải n8n. Proxy sử dụng HTTP/1.0 khi kết nối đến upstream hoặc loại bỏ các header upgrade, khiến quá trình upgrade không bao giờ xảy ra và trình biên tập liên tục kết nối lại. Hoặc quá trình upgrade thành công nhưng sau đó proxy đóng socket vì socket không có hoạt động trong một khoảng thời gian. WebSocket không có message trông giống hệt một kết nối không hoạt động. Trong cả hai trường hợp, container vẫn hoạt động bình thường. Banner chỉ cho biết trình duyệt đã mất kênh kết nối.

Hãy xác nhận nguyên nhân này trong trình duyệt trước khi chỉnh sửa cấu hình. Mở developer tools, chuyển đến tab Network, lọc theo WS rồi reload trình biên tập. Push request phải đến 101 Switching Protocols và duy trì trạng thái mở. Nếu push request trả về status code thông thường hoặc xuất hiện lại sau mỗi vài giây, nguyên nhân nằm ở proxy.

Các thiết lập nginx giúp editor duy trì kết nối

nginx không chuyển tiếp yêu cầu upgrade nếu bạn không cấu hình rõ. proxy_pass mặc định dùng HTTP/1.0 để kết nối đến backend, còn ConnectionUpgrade là các header hop-by-hop bị nginx xóa khi chuyển tiếp. Bạn phải thêm lại cả hai. Block map phải đặt trong context http, không đặt bên trong server.

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}
server {
    listen 443 ssl;
    http2 on;
    server_name n8n.example.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 $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 3600s;
        proxy_send_timeout 3600s;
        proxy_buffering off;
    }
}

proxy_read_timeout là dòng thường bị bỏ sót. Giá trị mặc định là 60 giây và cũng áp dụng cho WebSocket đã upgrade, nên tab editor để mở trên một instance không có hoạt động sẽ mất kết nối khoảng một phút sau khi message cuối cùng đi qua. Tăng giá trị này sẽ khắc phục banner xuất hiện khi bạn quay lại tab đã để mở.

sudo nginx -t && sudo systemctl reload nginx
sudo nginx -T | grep -iE 'proxy_http_version|upgrade|proxy_read_timeout'

nginx -T in toàn bộ cấu hình đang chạy thay vì chỉ một file, nhờ đó xác nhận được thay đổi của bạn đã được load. Một cấu hình nằm trong file mà không có dòng include đọc vào là lý do bản sửa đúng vẫn có vẻ không có tác dụng.

Sau đó cho n8n biết nó đang chạy phía sau proxy, vì n8n tạo URL dựa trên các giá trị này.

environment:
  - N8N_HOST=n8n.example.com
  - N8N_PROTOCOL=https
  - N8N_PORT=5678
  - N8N_PROXY_HOPS=1
  - N8N_WEBHOOK_URL=https://n8n.example.com/

N8N_PROXY_HOPS mặc định bằng 0. Điều này khiến n8n xem địa chỉ kết nối là địa chỉ client và bỏ qua X-Forwarded-For. Đặt giá trị này bằng số proxy nằm phía trước container. Tính đến August 2026, N8N_WEBHOOK_URL là tên hiện tại và WEBHOOK_URL cũ vẫn hoạt động, nhưng sẽ in cảnh báo deprecated khi khởi động.

Traefik chuyển tiếp WebSocket nhưng sau đó timeout

Traefik chuyển tiếp yêu cầu nâng cấp WebSocket mà không cần middleware hoặc label bổ sung. Vì vậy, người dùng Traefik thấy banner này thường đang gặp timeout, không phải thiếu header. Các tham số cần chỉnh nằm trên entryPoint. Tính đến tháng 8 năm 2026 trong Traefik v3, idleTimeout mặc định là 180 giây và readTimeout mặc định là 60 giây.

entryPoints:
  websecure:
    address: ":443"
    transport:
      respondingTimeouts:
        readTimeout: 0
        idleTimeout: 3600s

Caddy tự động xử lý việc nâng cấp trong reverse_proxy và không cần directive cho việc này. Nếu bạn hoàn toàn không thể thay đổi proxy vì proxy do người khác quản lý, hãy chuyển kênh push bằng N8N_PUSH_BACKEND=sse. SSE (server-sent events) là một HTTP response thông thường được giữ mở. Vì vậy, nó vẫn hoạt động qua proxy từ chối việc nâng cấp, nhưng idle timeout quá ngắn vẫn có thể ngắt kết nối. Việc chọn proxy là một quyết định riêng. Bài so sánh nginx, Caddy và Traefik trình bày chi phí vận hành của từng lựa chọn.

Khi container thực sự đang khởi động lại

Nếu RestartCount tăng, container đang gặp lỗi và Docker đang khởi động lại nó. Đối chiếu timestamp trong log với từng lần restart, rồi đọc nội dung xuất hiện ngay trước đó. Gần như mọi trường hợp đều thuộc 4 nguyên nhân: lỗi cấu hình khiến quá trình khởi động thất bại, database mà n8n không thể kết nối, tiến trình bị crash sau khi đã chạy, hoặc bị kill do thiếu memory.

Hãy bắt đầu với volume, vì quyền truy cập thường là nguyên nhân khó nhận biết. Image chính thức chạy bằng user không có đặc quyền node và lưu dữ liệu tại /home/node/.n8n. Bind mount được tạo bởi root không cho user đó quyền ghi, nên process luôn chết khi khởi động và restart policy che giấu lỗi bằng một vòng lặp.

docker compose config
docker run --rm -it --entrypoint sh docker.n8n.io/n8nio/n8n -c 'id'
docker exec n8n ls -ld /home/node/.n8n

Named volume tránh hoàn toàn vấn đề này vì Docker tạo volume với ownership phù hợp. Nếu cần dùng bind mount, chown thư mục trên host cho numeric user id mà command đầu tiên đã in ra. Bạn chỉ cần hiểu một lần cách ánh xạ ownership giữa host và container; bài giải thích về PUID và PGID trình bày cách các image này xác định user nào được ghi file.

Lỗi bị kill do hết bộ nhớ trông giống như crash

Có hai mức giới hạn bộ nhớ riêng biệt áp dụng cho một process n8n, và mỗi mức gây lỗi theo cách khác nhau. Giới hạn của control group trong container do kernel thực thi: khi vượt quá giới hạn, process bị kill ngay lập tức, không có cơ hội ghi bất kỳ thông tin nào, và OOMKilled trả về true. Giới hạn V8 heap được thực thi bên trong Node.js: khi vượt quá giới hạn này, Node tạo lỗi heap kèm stack trace rồi tự thoát, nên OOMKilled trả về false. Nhìn từ trình duyệt, hai trường hợp này giống hệt nhau. Trong docker inspect, chúng chỉ cách nhau một field.

Đặt giới hạn heap của Node thấp hơn giới hạn của container. Nếu giới hạn heap cao hơn trong hai giới hạn, V8 sẽ tiếp tục cấp phát bộ nhớ vượt qua điểm kernel can thiệp. Khi đó garbage collector không bao giờ chạm đến giới hạn của chính nó, và bạn luôn gặp lỗi nghiêm trọng hơn mà không có log để đọc.

services:
  n8n:
    image: docker.n8n.io/n8nio/n8n
    restart: unless-stopped
    environment:
      - NODE_OPTIONS=--max-old-space-size=<MiB, below the container limit>
    deploy:
      resources:
        limits:
          memory: <your container limit>

Chọn cả hai giá trị dựa trên tài nguyên thực tế của VPS, đồng thời chừa bộ nhớ cho database, proxy và hệ điều hành. docker stats --no-stream hiển thị mức sử dụng hiện tại cùng giới hạn đang có hiệu lực, để bạn kiểm tra rằng giới hạn đã cấu hình đúng là giới hạn Docker áp dụng. Cách áp dụng giới hạn bộ nhớ trong Compose giải thích key nào được ưu tiên khi có nhiều key cùng được đặt.

Dữ liệu thực thi là phần tiếp tục tăng bên dưới

Một lần thực thi chứa output của mọi node trong khi run đang diễn ra, sau đó n8n lưu dữ liệu đó. Có 2 hệ quả. Mức sử dụng memory đỉnh của một run phụ thuộc vào batch dữ liệu lớn nhất được truyền qua nó. Vì vậy, workflow xử lý cùng lúc 10000 dòng là một chương trình khác với workflow đó khi chỉ xử lý 200 dòng mỗi lần. Bản sao đã lưu cũng tiếp tục tăng cho đến khi có thành phần xóa nó.

Pruning xử lý vấn đề thứ hai. Tính đến tháng 8 năm 2026, mặc định là pruning được bật, EXECUTIONS_DATA_MAX_AGE ở mức 336 giờ (14 ngày) và EXECUTIONS_DATA_PRUNE_MAX_COUNT ở mức 10000. Đây là các mức khá rộng đối với một VPS nhỏ chạy SQLite, nơi một file chứa toàn bộ dữ liệu và cùng một process phục vụ editor phải đọc và ghi file đó.

environment:
  - EXECUTIONS_DATA_PRUNE=true
  - EXECUTIONS_DATA_MAX_AGE=72
  - EXECUTIONS_DATA_PRUNE_MAX_COUNT=1000
  - EXECUTIONS_DATA_SAVE_ON_SUCCESS=none
  - EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=false

EXECUTIONS_DATA_SAVE_ON_SUCCESS=none là thiết lập mạnh tay. Nó giữ lại các execution bị lỗi để debug và xóa các execution thành công. Hãy quyết định rõ ràng về việc này, vì một workflow có thể tạo output sai mà không phát sinh lỗi, rồi không để lại dữ liệu nào cho bạn kiểm tra. Pruning cũng chỉ đánh dấu các row là đã xóa trước, rồi xóa chúng trong một lần chạy sau. SQLite tái sử dụng các page đã được giải phóng thay vì trả chúng về hệ thống, nên file trên disk không thu nhỏ ngay khi bạn thay đổi thiết lập.

Để giảm mức đỉnh thay vì tổng dữ liệu đã lưu, hãy di chuyển ít dữ liệu hơn trong mỗi run. Chia các job lớn thành các sub-workflow trả về kết quả nhỏ cho workflow cha, dùng node Loop Over Items để chia batch, và không đưa toàn bộ dataset vào node Code.

Tệp nhị phân không nên nằm trong memory

N8N_DEFAULT_BINARY_DATA_MODE mặc định là default, khiến dữ liệu nhị phân được giữ trong memory của execution đang chạy. Mọi file mà một node tải xuống và mọi bản sao được chuyển cho node tiếp theo đều nằm đó cho đến khi run kết thúc. Một workflow tải vài attachment lớn có thể khiến process vượt quá giới hạn mà các tác vụ JSON thông thường không bao giờ chạm tới. Vì vậy, sự cố xảy ra với một workflow cụ thể thay vì theo thời gian chạy.

environment:
  - N8N_DEFAULT_BINARY_DATA_MODE=filesystem

Với filesystem, dữ liệu nhị phân được ghi vào N8N_BINARY_DATA_STORAGE_PATH. Theo mặc định, thư mục này nằm trong thư mục người dùng của n8n, nên được lưu trên cùng volume với mọi dữ liệu khác. Hãy kiểm tra volume còn đủ dung lượng trước khi chuyển đổi. N8N_PAYLOAD_SIZE_MAX đặt kích thước tối đa của payload webhook đi vào theo MiB (mebibyte), mặc định là 16. Tăng giá trị này cho phép nhận các request lớn hơn, đồng thời làm tăng mức sử dụng memory mà bạn chấp nhận.

Các dịch vụ khác dùng chung máy chủ cũng cạnh tranh cùng một lượng RAM. Nếu các lần OOM kill bắt đầu sau khi bạn thêm một database container, chạy database trong Docker hoặc trên host là đánh đổi mà bạn đang lựa chọn.

Chính sách restart và cách container hoạt động lại sau khi reboot

Container không có restart policy sẽ dừng hẳn sau khi thoát và vẫn ở trạng thái đó sau khi host reboot. restart: unless-stopped khởi động lại container trong cả hai trường hợp, nhưng vẫn tôn trọng container mà bạn đã dừng thủ công. restart: always cũng khởi động lại container mà bạn đã chủ động dừng, ngay khi Docker khởi động lần tiếp theo.

n8n cung cấp một health endpoint được xác định bằng N8N_ENDPOINT_HEALTH, mặc định là healthz. Trước tiên, hãy kiểm tra endpoint này từ host để xác nhận đường dẫn trên instance của bạn là chính xác.

curl -fsS http://127.0.0.1:5678/healthz
docker exec n8n which wget curl
sudo systemctl is-enabled docker

Healthcheck tự nó không khởi động lại gì. Compose đánh dấu container là không khỏe rồi dừng tại đó, vì vậy healthcheck cần đi kèm restart policy hoặc một watcher bên ngoài thì mới có tác dụng. Viết healthcheck thực sự có tác dụnglàm cho stack khởi động lại sau khi reboot trình bày cả hai phần.

Workflow không bao giờ chạy dù n8n vẫn hoạt động

Workflow này không hiển thị banner và cũng không restart. Container vẫn đang chạy, editor hoạt động, nhưng lần chạy bạn chờ đợi không xuất hiện trong danh sách executions. Phần lớn trường hợp bắt nguồn từ 4 nguyên nhân.

  • Workflow chưa được active. Schedule Trigger chỉ chạy trên production path, nên việc test trong canvas không tạo lịch chạy nào.
  • Timezone không phải múi giờ của bạn. GENERIC_TIMEZONE mặc định là America/New_York, nên schedule đặt lúc 09:00 sẽ chạy lúc 09:00 theo múi giờ đó cho đến khi bạn đặt GENERIC_TIMEZONETZ thành múi giờ của mình.
  • Thời gian downtime không được chạy bù sau đó. Trigger được đăng ký khi n8n khởi động, nên schedule đến hạn trong lúc container đang restart sẽ không chạy muộn. Lần chạy tiếp theo là thời điểm đến hạn tiếp theo sau khi khởi động.
  • Workflow đã bị deactivated tự động. N8N_WORKFLOW_AUTODEACTIVATION_ENABLED mặc định đang tắt. Khi bật tùy chọn này, workflow liên tục crash sẽ bị unpublished. Sau đó, workflow trông giống hệt workflow chưa từng được active.

Mở danh sách executions và lọc theo workflow đó. Nếu có một entry bị failed, vấn đề nằm ở workflow. Nếu hoàn toàn không có entry nào, vấn đề nằm ở trigger; hãy kiểm tra 4 nguyên nhân trên.

Cần thay đổi gì trước tiên

  1. Tự đọc STATUS, RestartCountOOMKilled trên container của bạn trước khi chỉnh sửa bất kỳ file nào.
  2. Nếu container chưa từng dừng, hãy sửa các proxy upgrade header và idle timeout.
  3. Nếu OOMKilledtrue, hãy đặt một giới hạn container do bạn chủ động chọn, đặt giới hạn heap của Node thấp hơn giới hạn đó, rồi chuyển dữ liệu nhị phân sang filesystem.
  4. Nếu không có gì được kích hoạt, hãy kiểm tra workflow có đang active không và timezone của instance có đúng với timezone của bạn không.

Phần lớn nội dung này là cấu hình chỉ cần đặt một lần rồi để đó, với điều kiện hệ thống đã được cài đặt và hoạt động. Nếu bạn vẫn đang hoàn thiện phần cài đặt, hướng dẫn chạy n8n trên Docker với HTTPS là nền tảng để áp dụng các thiết lập này.

FAQ

Vì sao editor n8n hiển thị thông báo mất kết nối dù container vẫn đang chạy?

Editor duy trì một kết nối WebSocket để truyền tiến độ thực thi. Nếu reverse proxy không chuyển tiếp các header Connection: UpgradeUpgrade: websocket, hoặc không dùng HTTP/1.1 cho upstream, quá trình nâng cấp sẽ không hoàn tất. Khi đó trình duyệt sẽ liên tục kết nối lại trong khi n8n vẫn hoạt động bình thường. Trong nginx, bạn cần proxy_http_version 1.1 cùng cả hai dòng proxy_set_header, và một proxy_read_timeout dài hơn 60 giây mặc định để tab không hoạt động không bị ngắt. Kiểm tra cấu hình đang chạy bằng sudo nginx -T, không phải file bạn đã chỉnh sửa.

Làm thế nào phân biệt tiến trình bị kill do hết bộ nhớ với một lỗi crash thông thường?

Chạy docker inspect n8n | grep -iE 'OOMKilled|ExitCode|RestartCount' và đọc cờ OOMKilled. Giá trị True nghĩa là kernel đã kill tiến trình vì vượt giới hạn bộ nhớ. Khi đó container log sẽ không có thông tin hữu ích vì tiến trình không kịp ghi log. Giá trị False, kèm lỗi heap và stack trace ở cuối docker logs, nghĩa là Node.js đã chạm giới hạn V8 heap của chính nó rồi tự thoát. Đặt NODE_OPTIONS=--max-old-space-size thấp hơn giới hạn của container để nhận được lỗi thứ hai. Đây là lỗi để lại thông tin chẩn đoán.

Pruning execution data có giải phóng dung lượng disk ngay không?

Không. EXECUTIONS_DATA_PRUNE đánh dấu các execution cũ để xóa, rồi một lần xử lý sau đó mới xóa chúng theo lịch được đặt bằng EXECUTIONS_DATA_PRUNE_HARD_DELETE_INTERVAL. Với SQLite, file cũng tái sử dụng các page đã được giải phóng thay vì trả chúng về filesystem. Vì vậy kích thước file trên disk có thể vẫn giữ nguyên trong một thời gian sau khi các row đã bị xóa. Đặt EXECUTIONS_DATA_MAX_AGEEXECUTIONS_DATA_PRUNE_MAX_COUNT theo mức phù hợp với máy chủ của bạn, rồi kiểm tra lại vào ngày hôm sau thay vì kiểm tra ngay.

Vì sao scheduled workflow không chạy khi n8n đang restart?

n8n đăng ký các trigger khi process khởi động. n8n không chạy bù các schedule đến hạn trong thời gian process dừng. Vì vậy, một vòng lặp restart sẽ tạo ra khoảng lặng thay vì một loạt lần chạy bù. Lần chạy tiếp theo sẽ là thời điểm đến hạn kế tiếp sau khi process khởi động. Nếu cần bảo đảm không bỏ lỡ lần chạy, hãy điều khiển workflow từ một caller bên ngoài gọi vào webhook. Khi đó logic retry nằm bên ngoài n8n.

Healthcheck có restart n8n khi n8n ngừng phản hồi không?

Không tự nó. Healthcheck của Compose chỉ đánh dấu container là healthy hoặc unhealthy. Việc restart do restart policy đảm nhiệm. Vì vậy restart: unless-stopped sẽ đưa container chạy lại sau khi container thoát. Policy này cũng đưa container chạy lại sau khi host reboot, miễn là Docker service đã được enable. Xác nhận bằng sudo systemctl is-enabled docker. Nếu muốn xử lý riêng trạng thái unhealthy, bạn cần một watcher bên ngoài Docker để đọc trạng thái và restart service.