SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-22

n8n liên tục offline trên VPS: cách tìm đúng lỗi

n8n báo mất kết nối có thể do WebSocket, restart loop, OOM kill hoặc lịch chạy chết. Phân biệt từng lỗi trước khi sửa Docker, proxy hay memory.

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, và mỗi lỗi cần một cách xử lý riêng. Editor hiển thị banner mất kết nối trong khi container vẫn 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 dùng quá nhiều memory. Hoặc thực tế process không có lỗi nào, nhưng một workflow đang active 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 chỉnh 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 layer có thể lỗi theo cách riêng, nhưng browser báo tất cả các lỗi đó 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ị do 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ị quan trọng ở đây 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 trong docker ps -a cho biết container đã ở trạng thái hiện tại bao lâu. So sánh thời điểm đó với lúc 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. Đây là websocket path đượ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 khi bạn theo dõi thì container đang ở trong restart loop. 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 flag 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 limit của toàn bộ máy. Trường này giúp phân biệt việc bị kill do thiếu memory với mọi kiểu exit khác. Vì vậy, hãy đọc nó trước khi phỏng đoán.

ExitCode là exit code gần nhất của container. Bạn không cần ghi nhớ ý nghĩa của từng code. Đọc code của bạn, sau đó đọc phần cuối của docker logs tại cùng timestamp. Phần cuối log và out of memory flag khi xem cùng nhau sẽ cho biết điều 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 ngay cạnh limit hiện tại. Để lệnh này chạy trong terminal thứ hai, kích hoạt workflow gây ra sự cố và theo dõi giá trị thay đổi thế nào trong lúc lỗi xảy ra.


Trình chỉnh sửa n8n duy trì một push connection mở lâu dài đến backend để truyền tiến trình thực thi lên canvas. Mặc định, connection này là WebSocket. Đây là loại connection được N8N_PUSH_BACKEND chọn và giá trị mặc định của nó là websocket. WebSocket bắt đầu bằng một HTTP request thông thường, mang các header Connection: Upgrade và Upgrade: websocket. Server trả về 101 Switching Protocols. Sau đó, hai phía 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 connection này hỏng. Cả hai đều nằm ở proxy, không phải ở n8n. Proxy dùng HTTP/1.0 khi kết nối đến upstream hoặc xóa các header upgrade. Vì vậy, quá trình upgrade không diễn ra và trình chỉnh sửa liên tục reconnect. Hoặc upgrade thành công nhưng sau đó proxy đóng socket vì socket không có hoạt động trong một thời gian. WebSocket không có message trông giống hệt một connection idle. Trong cả hai trường hợp, container vẫn healthy. Banner chỉ cho biết browser đã mất channel đến backend.

Hãy xác nhận nguyên nhân này trong browser 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 chỉnh sửa. Push request phải đến 101 Switching Protocols và tiếp tục 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 nhiều khả năng nằm ở proxy.

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

nginx không forward yêu cầu upgrade nếu bạn không cấu hình rõ. Theo mặc định, proxy_pass dùng HTTP/1.0 với backend, còn Connection và Upgrade là các hop-by-hop header bị nginx xóa khi chuyển tiếp. Bạn phải thêm lại cả hai. Block map phải nằm trong context http, không được đặt bên trong server. Nếu phần còn lại của server block bên dưới chưa quen thuộc, phần giải thích từng dòng về server block của nginx trình bày chức năng của từng directive.

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. Vì vậy, một tab editor mở trên instance không có hoạt động sẽ mất kết nối khoảng một phút sau khi tin nhắn cuối cùng đi qua. Tăng giá trị này sẽ sửa 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ộ running configuration thay vì chỉ một file. Nhờ đó, bạn xác nhận được thay đổi của mình thực sự đã được load. Nếu file cấu hình không nằm trong đường dẫn mà dòng include đọc, 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/

Mặc định N8N_PROXY_HOPS là 0. Điều này có nghĩa là 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. Tên cũ WEBHOOK_URL vẫn hoạt động nhưng sẽ in cảnh báo deprecation khi khởi động.

Traefik chuyển tiếp WebSocket rồi ngắt do 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 chứ không phải thiếu header. Các tùy chọn này nằm trên entryPoint. Tính đến tháng 08 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ở, nên vẫn hoạt động với proxy từ chối nâng cấp. Tuy nhiên, 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 restart

Nếu RestartCount tăng, container đang bị 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 phần 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 crash sau khi đã chạy, hoặc bị kill do thiếu memory.

Hãy bắt đầu với volume, vì lỗi quyền thường khó nhận ra. 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 chết ở mỗi lần 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ệnh kill do hết bộ nhớ trông giống như một lần crash

Có 2 giới hạn bộ nhớ độc lập áp dụng lên một tiến trình n8n, và chúng lỗi theo những cách khác nhau. Giới hạn của control group trong container do kernel thực thi: khi vượt qua giới hạn này, tiến trình bị kill ngay lập tức, không có cơ hội ghi lại thông tin nào, và OOMKilled trả về true. Giới hạn heap của V8 do Node.js thực thi: khi vượt qua giới hạn này, Node phát sinh lỗi heap kèm stack trace rồi tự thoát, nên OOMKilled trả về false. Nhìn từ browser, 2 trường hợp này giống hệt nhau. Trong docker inspect, chúng khác nhau ở đúng 1 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 2 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ả 2 giá trị dựa trên tài nguyên thực tế của VPS, đồng thời chừa lại 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 áp dụng, giúp bạn kiểm tra rằng giới hạn đã khai báo đúng là giới hạn Docker áp dụng. Cách Compose áp dụng giới hạn bộ nhớ giải thích key nào có hiệu lực khi đặt nhiều giới hạn cùng lúc.

Dữ liệu execution là thứ âm thầm phình to

Một execution chứa output của mọi node trong suốt thời gian run, sau đó n8n lưu lại dữ liệu đó. Có 2 hệ quả. Mức memory peak của một run phụ thuộc vào batch dữ liệu lớn nhất được truyền qua workflow. Vì vậy, workflow xử lý 10000 row cùng lúc là một chương trình khác với chính workflow đó khi chỉ xử lý 200 row mỗi lần. Bản sao được 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 08 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. Các mức này 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à chính 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 tích cực. Thiết lập này 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 có chủ ý, vì nếu workflow tạo ra output sai nhưng không phát sinh error thì sau đó bạn sẽ không còn gì để kiểm tra. Pruning cũng đánh dấu các row là đã xóa trước, rồi xóa chúng trong 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ệ điều hành, nên file trên disk không thu nhỏ ngay khi bạn thay đổi thiết lập.

Để giảm mức peak thay vì tổng dữ liệu được lưu, hãy truyề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ề result nhỏ cho workflow cha, dùng node Loop Over Items để batch, và không đưa toàn bộ dataset vào node Code.

Tệp nhị phân không nên đi qua 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 chỉ cần tải vài attachment lớn cũng có thể đẩy 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, crash 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. Thư mục này mặc định nằm trong thư mục người dùng của n8n, nên dữ liệu sẽ nằm trên cùng volume với mọi thứ 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 gửi vào theo MiB (mebibyte) và mặc định là 16. Tăng giá trị này cho phép nhận các request lớn hơn, nhưng bạn phải chấp nhận chi phí memory tương ứng.

Mọi thành phần khác dùng chung máy chủ đều tranh chấp cù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 bạn đang lựa chọn.

Restart policy và việc khởi động lại sau reboot

Container không có restart policy sẽ dừng hẳn sau khi thoát và vẫn không chạy lạ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 đã được bạn dừng cố ý vào lần Docker khởi động tiếp theo.

n8n cung cấp một health endpoint, được xác định bằng N8N_ENDPOINT_HEALTH, với giá trị mặc định là healthz. Trước tiên, hãy kiểm tra endpoint này từ host để xác nhận path 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ì cả. Compose đánh dấu container là unhealthy rồi dừng ở đó. 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ụng và để stack khởi động lại sau 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 mong đợ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.
  • Timezone không phải múi giờ của bạn. GENERIC_TIMEZONE mặc định là America/New_York, vì vậy schedule đặt lúc 09:00 sẽ chạy lúc 09:00 theo múi giờ đó cho đến khi bạn đặt GENERIC_TIMEZONE và TZ theo múi giờ của mình.
  • Thời gian downtime không được chạy bù. 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 sau đó. Lần chạy tiếp theo là thời điểm đến hạn kế tiếp 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 đó, nó 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 workflow failed với lỗi 429 khi gọi một service khác mà bạn host trên cùng máy, giới hạn đó thuộc về service kia chứ không phải n8n. Bài hướng dẫn xử lý lỗi 429 của SearXNG giải thích cách phân biệt rate limiter của chính service đó với các engine đang chặn IP server của bạn. 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 trước.

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

  1. Tự đọc STATUS, RestartCount và OOMKilled trong 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 header upgrade của proxy và idle timeout.
  3. Nếu OOMKilled đúng, hãy đặt giới hạn cho container một cách có chủ đích, đặt ngưỡng heap của Node thấp hơn giới hạn đó và 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 để đó, trên một bản cài đặt đang hoạt động. Nếu bạn vẫn đang dựng bả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ị banner mất kết nối khi container vẫn đang chạy?

Editor duy trì một WebSocket để truyền tiến trình thực thi. Nếu reverse proxy không chuyển tiếp các header Connection: Upgrade và Upgrade: websocket, hoặc không dùng HTTP/1.1 ở upstream, quá trình nâng cấp sẽ không hoàn tất. Trình duyệt sẽ kết nối lại liên tục trong khi n8n vẫn hoạt động bình thường. Với nginx, bạn cần proxy_http_version 1.1 cùng với 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 sao phân biệt tiến trình bị kill do hết bộ nhớ với một crash thông thường?

Chạy docker inspect n8n | grep -iE 'OOMKilled|ExitCode|RestartCount' và đọc flag OOMKilled. Giá trị True nghĩa là kernel đã kill tiến trình vì vượt quá memory limit. Khi đó log của container sẽ không có thông tin hữu ích vì tiến trình không có cơ hội 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ó và 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.

Prune execution data có giải phóng disk space ngay không?

Không. EXECUTIONS_DATA_PRUNE đánh dấu các execution cũ để xóa, rồi một lượt 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 vẫn giữ nguyên trong một thời gian sau khi các row đã bị xóa. Đặt EXECUTIONS_DATA_MAX_AGE và EXECUTIONS_DATA_PRUNE_MAX_COUNT theo giá trị 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 của tôi không chạy khi n8n đang restart?

n8n đăng ký các trigger khi process khởi động. Nó không chạy lại các schedule đến hạn trong thời gian process bị dừng. Vì vậy, một vòng lặp restart sẽ không tạo ra một loạt lần chạy bù. Lần execution tiếp theo là thời điểm đến hạn tiếp theo sau khi startup hoàn tất. 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 đó retry logic nằm bên ngoài n8n.

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

Không, nếu chỉ dùng healthcheck. 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 hoạt động lại sau khi container thoát. Nó cũng đưa container hoạt động lại sau khi host reboot, miễn là Docker service đã được enable. Xác nhận điều này 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.