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

Chạy Claude Code trên VPS qua tmux không mất phiên

Chạy Claude Code trên Linux VPS luôn bật trong tmux để phiên agent vẫn sống khi SSH rớt. Có hướng dẫn cài đặt, harden và lỗi SIGHUP cần biết.

Vấn đề là nắp laptop, không phải CLI

Claude Code chạy ổn trên laptop cho đến khi bạn đóng nắp: phiên SSH bị ngắt, shell nhận SIGHUP và agent đang chạy được ba phút trong một lần test cũng dừng theo. Hãy chạy CLI trên một máy không bao giờ sleep, bên trong một terminal multiplexer có các process không phải là process con của phiên SSH. Đó là toàn bộ mẹo; tmux mới là phần quan trọng, không phải bước cài đặt.

Trang này hướng dẫn vận hành một máy chủ để bạn để agent chạy trên đó. Nếu bạn không có Linux server có thể để bật liên tục, các nội dung này không áp dụng. Đó là điều kiện tiên quyết duy nhất cần nói rõ.

tmux thực sự làm gì

Khi bạn SSH vào máy chủ, sshd tạo một shell con và cấp cho nó một pseudo-terminal; mọi tiến trình bạn khởi chạy từ shell đó đều là tiến trình con của nó. Nếu kết nối bị ngắt, kernel sẽ hủy pty, shell nhận SIGHUP rồi lần lượt gửi tín hiệu hangup đến các tiến trình con. Các tiến trình chạy lâu ở foreground sẽ dừng.

tmux thay đổi cách quản lý này. Lệnh tmux bạn nhập là một thin client giao tiếp qua unix socket với một tmux server chạy tách khỏi terminal của bạn. Các shell bên trong session là tiến trình con của server đó, không phải của sshd. Khi kết nối SSH bị ngắt, client biến mất nhưng server, session và agent đang chạy vẫn tiếp tục hoạt động. Khi kết nối lại, tmux attach, bạn trở về đúng shell trước đó cùng với scrollback cũ. nohup cũng tồn tại sau khi bị hangup, nhưng không cung cấp cách để truy cập lại; bạn không thể re-attach vào một TUI đã chạy nền. Claude Code là ứng dụng interactive; tmux (hoặc screen) là công cụ phù hợp.

Cấu hình máy chủ

CLI là một tiến trình Node; bản thân nó không làm đầy máy. Thứ làm đầy máy là mọi tác vụ agent chạy thay bạn: một lần build, toàn bộ test suite, tsc, language server hoặc database trong Docker. Hãy chọn cấu hình theo toolchain, không phải theo CLI. Hãy thêm swap ngay cả khi bạn dự định không bao giờ dùng đến. Swap biến một lần OOM kill nghiêm trọng thành một lần build chậm:

sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Cũng cần theo dõi dung lượng disk: repo, node_modules và Docker image có thể nhanh chóng chiếm hết dung lượng. Nếu toolchain sử dụng cả virtual machine thay vì chỉ container, chẳng hạn KVM guest hoặc Kubernetes node cục bộ, hãy kiểm tra plan có cung cấp các CPU virtualisation extension trước khi đăng ký. Chạy nested virtualization trên VPS là việc provider bật cho bạn, không phải thứ bạn có thể bật từ bên trong guest.

Tạo user không phải root trước tiên

Tạo một user riêng với home riêng, rồi đặt public key vào đó:

sudo adduser --disabled-password --gecos "" agent
sudo install -d -m 700 -o agent -g agent /home/agent/.ssh
sudo cp ~/.ssh/authorized_keys /home/agent/.ssh/authorized_keys
sudo chown agent:agent /home/agent/.ssh/authorized_keys
sudo chmod 600 /home/agent/.ssh/authorized_keys

Hãy kiểm tra đăng nhập từ một terminal thứ hai trước khi tiếp tục, khi password authentication vẫn còn để dự phòng. Nếu kết quả là Bị từ chối quyền (publickey), nguyên nhân thường nằm ở ownership hoặc mode của thư mục .ssh, không phải ở key.

Cố ý không đưa agent vào group sudo. Nếu cần một system package, hãy cài package đó. Một quyết định này loại bỏ phần lớn khả năng một lệnh shell ngoài ý muốn làm hỏng host.

Vệ sinh SSH cho máy chủ để chạy liên tục

Không nên duy trì xác thực bằng password trên máy kết nối Internet công khai cả ngày, đang chứa agent và source code của bạn. Hãy tắt nó. Trên Ubuntu 24.04 và Debian 13, /etc/ssh/sshd_config đã bao gồm /etc/ssh/sshd_config.d/*.conf, nên hãy thêm một file riêng thay vì sửa file cấu hình chính:

# /etc/ssh/sshd_config.d/10-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no

Kiểm tra cấu hình rồi reload. Giữ phiên hiện tại mở trong khi dùng terminal thứ hai để thử một phiên mới:

sudo sshd -t && sudo systemctl restart ssh

Lưu ý trên Ubuntu 24.04: sshd được kích hoạt bằng socket. Các thiết lập xác thực có hiệu lực trên systemctl restart ssh, nhưng thay đổi đối với Port cũng cần systemctl daemon-reload và restart ssh.socket.

Tiếp theo là firewall. Hãy cho phép SSH trước khi bật firewall, nếu không bạn sẽ tự khóa mình khỏi máy chủ:

sudo ufw allow OpenSSH
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable

Cài fail2ban nhưng cần hiểu rõ lợi ích của nó: khi đã tắt xác thực bằng password, brute force cũng không thể đăng nhập thành công; fail2ban chỉ giúp loại các lần thử thất bại khỏi journal.

# /etc/fail2ban/jail.local
[sshd]
enabled = true
backend = systemd
maxretry = 5
bantime = 1h

Cuối cùng, hãy tự động cài patch bằng sudo apt install unattended-upgradessudo dpkg-reconfigure -plow unattended-upgrades. Lưu ý tương tác với tmux: bật Unattended-Upgrade::Automatic-Reboot sẽ khiến kernel update reboot máy chủ, đồng thời kết thúc mọi phiên. Nếu để tắt, bạn có thể reboot theo lịch của mình, khi không có tác vụ nào đang chạy giữa chừng. Cần thận trọng tương tự với release upgrade: nâng máy chủ từ Ubuntu 24.04 lên 26.04 sẽ restart sshd và kernel, nên thực hiện trong thời gian bảo trì khi không có tmux session nào đang giữ công việc quan trọng.

Cài đặt Node.js và Claude Code trên Ubuntu

Claude Code là một Node CLI, nên bạn cần phiên bản Node hiện tại. Gói của distro thường chậm cập nhật; NodeSource là cách thường dùng trên Ubuntu và Debian, đồng thời cung cấp một repo có chữ ký (không cần apt-key, công cụ đó đã bị loại bỏ):

curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
node --version

Đây là phần nhiều người làm sai: cài CLI bằng user agent của bạn, không bao giờ dùng sudo npm -g. Global prefix thuộc về root sẽ gây lỗi quyền sau này và để lại các file thuộc về root trong npm cache. Trước tiên, hãy trỏ prefix của npm vào home của user:

mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
npm install -g @anthropic-ai/claude-code
claude --version

Đặt export trong ~/.bashrc, không phải ~/.profile, và đặt nó bên trên guard "If not running interactively, don't do anything" gần đầu file: tmux có thể khởi động các shell không phải login shell. Các shell này đọc ~/.bashrc và bỏ qua ~/.profile; ~/.profile chỉ chạy với login shell. Dùng Node per-user thông qua version manager như nvm cũng đạt được kết quả tương tự; dù dùng cách nào, mục tiêu là npm install -g không bao giờ cần sudo. npm vẫn hoạt động bình thường. Bạn cũng có thể dùng native install script của Anthropic, hiện là cách được tài liệu chính thức khuyến nghị. Hãy kiểm tra tài liệu cài đặt của Anthropic trước khi dán lệnh, vì các phương thức cài đặt có thể thay đổi.

Chạy claude bên trong một repo để khởi động. Lần chạy đầu tiên sẽ hướng dẫn bạn xác thực; máy headless không có browser, nên quy trình sẽ cung cấp một URL để bạn mở trên máy của mình và một code để nhập lại vào terminal. (Cách khác là dùng API key trong environment.) Dù dùng cách nào, credential đó sẽ nằm trên server, dẫn đến phần mà nhiều người bỏ qua.

Câu chuyện về phạm vi ảnh hưởng

Một agent có quyền shell thực chất là một shell. Nó có thể đọc mọi thứ mà user đang chạy nó có quyền đọc, và push đến mọi nơi mà user đó có quyền push. Đây không phải là khuyết điểm của tool. Đó là bản chất của nó. Vì vậy, account mà agent chạy dưới đó quan trọng hơn bất kỳ setting riêng lẻ nào.

  • User riêng, không có quyền đặc biệt. Không thuộc group sudo, không dùng chung home directory với account của bạn.
  • Không để credential production trên máy. Không có ~/.aws/credentials chứa key production, không copy .env từ production xuống, không có database password có quyền ghi vào bất kỳ hệ thống quan trọng nào. Cấp cho agent credential staging hoặc chỉ-đọc.
  • Token có phạm vi giới hạn. Dùng GitHub token fine-grained chỉ giới hạn ở một repository; dùng deploy key nếu chỉ cần quyền đọc.

Claude Code cung cấp một flag để bỏ qua hoàn toàn các prompt yêu cầu quyền. Trên laptop hoặc project dùng thử, quyết định này tùy bạn. Trên server chứa token, nó loại bỏ lớp bảo vệ cuối cùng giữa một instruction bị hiểu sai và git push --force. Các prompt bị bỏ qua cũng không chỉ có lựa chọn bật hoặc tắt toàn bộ. Khi auto mode sắp trở thành mặc định mới, bạn nên biết server mà mình không trực tiếp theo dõi nên ghim ở permission mode nào. Flag này thực sự thay đổi điều gì, và cách giới hạn agent chạy với flag đó, từ sandbox tích hợp sẵn đến một VPS dùng một lần, được trình bày trong chạy Claude Code an toàn trên server.

Deploy key và SSH agent forwarding

Bạn có thể muốn ssh -A để git sử dụng key trên laptop. Hãy hiểu rõ quyền được cấp: agent forwarding expose socket của SSH agent cục bộ cho các process chạy dưới user đó trên máy chủ. Bất kỳ process nào chạy dưới agent, bao gồm cả agent, đều có thể yêu cầu key của bạn ký cho bất kỳ host nào mà process đó truy cập được, miễn là bạn vẫn đang kết nối. Quyền này lớn hơn rất nhiều so với việc “cho git pull repository này”.

Thay vào đó, hãy generate key trên server, đăng ký key đó làm deploy key cho từng repository (chỉ cấp quyền ghi nếu agent cần push), rồi cấu hình git identity để nhận diện được các commit từ máy chủ:

ssh-keygen -t ed25519 -C "agent deploy key" -f ~/.ssh/id_ed25519_repo
cat ~/.ssh/id_ed25519_repo.pub   # paste into the repo's Deploy Keys
git config --global user.name "Agent (build box)"
git config --global user.email "agent@example.com"

Quy trình làm việc với tmux

Cài đặt tmux (sudo apt install tmux), sau đó tạo một cấu hình tối thiểu ~/.tmux.conf:

set -g mouse on
set -g history-limit 50000
set -g default-terminal "tmux-256color"

Bốn lệnh này đáp ứng nhu cầu sử dụng hằng ngày:

tmux new -A -s claude     # attach to session "claude", creating it if absent
# ...run `claude` inside it, work normally...
# Ctrl-b then d           -> detach; everything keeps running
tmux ls                   # list sessions
tmux attach -t claude     # reattach, from this machine or any other
tmux kill-session -t claude

tmux new -A -s claude là lệnh cần ghi nhớ. Lệnh này attach vào session nếu session đã tồn tại và tạo session mới nếu chưa có. Vì vậy, một lệnh duy nhất đáp ứng cả việc bắt đầu ngày làm việc và tiếp tục công việc sau khi kết nối bị ngắt. Hãy tạo alias cho lệnh này. Trong một session, Ctrl-b c mở một window, Ctrl-b nCtrl-b p chuyển qua lại giữa các window, còn Ctrl-b [ vào copy mode để cuộn xem nội dung trước đó (q để thoát).

Có một điều cần biết về các session mà bạn không bao giờ kill: agent gửi lại toàn bộ conversation trong mỗi lượt, vì vậy hãy đọc Claude Code sử dụng token cho việc gì trong một session chạy lâu trước khi để một session chạy liên tục trong một tuần.

Các tình huống lỗi

"Session của tôi biến mất." tmux ls in no server running on /tmp/tmux-1000/default. Gần như luôn có nghĩa là tiến trình chưa từng chạy trong tmux: bạn SSH vào, chạy trực tiếp claude, rồi kết nối bị ngắt khiến tiến trình bị dừng. Không có gì để khôi phục. Thói quen ngăn lỗi này: tmux new -A -s <project> phải là lệnh đầu tiên sau mỗi lần đăng nhập.

Pane bị thu nhỏ thành một ô rất nhỏ. tmux lấy kích thước session theo client đang attach nhỏ nhất. Vì vậy, một client cũ vẫn còn attach từ máy khác có thể làm màn hình bị thu nhỏ. Buộc các client khác ngắt kết nối khi attach bằng: tmux attach -d -t claude.

Build in Killed. Chỉ có một từ và không có stack trace. Xác nhận bằng sudo dmesg -T | grep -i -E 'out of memory|killed process'; kernel OOM killer đã chọn tiến trình lớn nhất. Với Node, bạn có thể thay vào đó thấy FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory. Cách khắc phục theo thứ tự: thêm swap (như phần trên), giới hạn mức song song của test và compiler, tăng heap của Node bằng NODE_OPTIONS=--max-old-space-size=..., hoặc nâng cấp VPS. OOM killer cũng có thể chọn tmux server thay vì build, khiến session của bạn biến mất theo. Nếu systemd-oomd đang chạy, nó có thể kill toàn bộ user slice và gây ra kết quả tương tự.

npm error code EACCES / permission denied, mkdir '/usr/lib/node_modules/...'. Bạn đã cài global vào prefix thuộc sở hữu của root. Dùng prefix ~/.npm-global ở phần trên. Nếu trước đó bạn đã chạy sudo npm, bạn cũng có thể thấy Your cache folder contains root-owned files. Sửa bằng sudo chown -R $(id -u):$(id -g) ~/.npm.

claude: command not found, nhưng chỉ xảy ra đôi lúc. Export PATH của bạn nằm trong ~/.bashrc, bên dưới guard "Nếu không chạy tương tác thì không làm gì", nên các shell không tương tác sẽ bỏ qua nó. Chuyển export lên trên guard đó và giữ nó trong ~/.bashrc, không phải ~/.profile: tmux có thể khởi động các shell không phải login shell. Các shell này đọc ~/.bashrc và không bao giờ đọc ~/.profile.

Màu hiển thị bị lỗi sau khi attach. Đây là lỗi không khớp TERM. Dòng default-terminal ở trên là cách sửa.

Session biến mất sau khi reboot. Đây không phải bug: tmux server là một tiến trình và reboot sẽ kết thúc tiến trình đó. Kiểm tra uptime.

Những điểm sẽ hỏng khi hệ thống phát triển

Nhiều project hơn. Dùng một tmux session cho mỗi repo và đặt tên session theo repo đó; tmux ls khi đó là dashboard của bạn. Nếu không duy trì quy tắc đặt tên, bạn sẽ có các session 0, 1, 2. Khi nhiều session chạy đồng thời, chúng không nhất thiết phải hoạt động độc lập, vì một session có thể gửi message cho session khác trên cùng máy. Điều này hữu ích khi agent đang thực hiện một đợt refactor dài và muốn agent thứ hai chạy test. Port cũng tăng theo cách tương tự. Khi 6 repo cùng muốn dùng :3000, đó là lúc nên dừng việc gán port thủ công và để reverse proxy Traefik định tuyến nhiều app trong Docker Compose theo hostname.

Nhiều người hơn. tmux socket thuộc từng user, vì vậy 2 developer trên cùng máy sẽ có tmux server riêng và không thể nhìn thấy session của nhau. Chia sẻ một session qua shared socket nghĩa là mọi người cùng nhập lệnh vào cùng một shell dưới cùng Unix user, kéo theo các vấn đề về audit và quyền truy cập. Tách riêng user là lựa chọn đơn giản và đúng.

Công việc không có người giám sát. tmux dành cho các session tương tác mà bạn attach vào. Các job chạy theo lịch mà không có người theo dõi nên nằm trong systemd unit và timer. Chúng có sẵn logging, restart policy và khả năng tiếp tục chạy sau khi boot. Dùng tmux để chạy một job dạng cron là dấu hiệu cho thấy job đó nên được chuyển thành service.

Một điểm cuối: hãy bind các dev server do agent khởi động vào 127.0.0.1, không phải 0.0.0.0, rồi truy cập chúng qua SSH tunnel (ssh -L 3000:127.0.0.1:3000 agent@your-server) thay vì mở port trong ufw. Khi bạn forward khoảng 6 port, hoặc cả điện thoại và laptop đều muốn truy cập cùng một preview, hãy đặt WireGuard VPN tự host trên VPS ở phía trước thay thế: dev server bind vào private interface, còn ufw tiếp tục từ chối mọi kết nối từ interface public. Firewall chỉ có tác dụng nếu bạn không liên tục mở lỗ trên đó.

Claude Code không phải lựa chọn duy nhất: chạy coding AI agent trên VPS cũng có thể cân nhắc Aider và Goose.

FAQ

Claude Code có tiếp tục chạy sau khi kết nối SSH bị ngắt không?

Chỉ khi bạn khởi động nó bên trong tmux. Tiến trình được khởi chạy trực tiếp từ SSH shell là tiến trình con của shell đó và sẽ dừng cùng pty khi kết nối bị ngắt. Trong tmux, shell thuộc về tmux server đã detach, nên agent vẫn tiếp tục xử lý giữa chừng và tmux attach đưa bạn trở lại cùng phần nội dung đã cuộn. Hãy chạy tmux new -A -s <project> làm lệnh đầu tiên sau mỗi lần đăng nhập để giải quyết vấn đề này.

Có nên cài CLI bằng sudo npm install -g không?

Không. Global prefix thuộc sở hữu của root sẽ gây lỗi EACCES trong những lần cài đặt sau và tạo các file thuộc sở hữu của root trong npm cache. Đặt prefix của npm thành ~/.npm-global (hoặc dùng version manager như nvm), cài đặt bằng user không có đặc quyền agent, rồi export ~/.npm-global/bin vào PATH từ ~/.bashrc, bên trên interactive guard. Nếu bạn đã chạy sudo npm một lần, hãy sửa cache bằng sudo chown -R $(id -u):$(id -g) ~/.npm.

ssh -A agent forwarding có an toàn trên máy chủ đang chạy agent không?

Nó cấp nhiều quyền hơn đáng kể so với nhu cầu của job. Agent forwarding expose socket của SSH agent cục bộ cho mọi tiến trình chạy dưới user đó. Vì vậy, bất kỳ tiến trình nào trên máy chủ cũng có thể yêu cầu key của bạn ký cho bất kỳ host nào mà máy chủ có thể kết nối đến, miễn là bạn vẫn còn attached. Hãy tạo một key ed25519 trên server và đăng ký key đó làm deploy key cho từng repository, chỉ cấp quyền ghi khi agent thực sự cần push.

Tại sao build của tôi chỉ in Killed?

Một từ duy nhất không có stack trace thường là kernel OOM killer. Xác nhận bằng sudo dmesg -T | grep -i -E 'out of memory|killed process'; từ Node, bạn có thể thấy JavaScript heap out of memory thay thế. Thực hiện lần lượt các cách khắc phục: thêm swapfile, giới hạn mức song song của test và compiler, tăng NODE_OPTIONS=--max-old-space-size=..., rồi nâng cấp VPS. Lưu ý rằng OOM killer có thể chọn tmux server thay vì build, khiến toàn bộ session của bạn bị dừng.

tmux hay systemd service?

tmux phù hợp với các session tương tác mà bạn attach vào, theo dõi và nhập lệnh. Đây chính xác là kiểu session của agent. Công việc chạy theo lịch và không có người theo dõi nên dùng systemd unit và timer. Bạn sẽ có sẵn logging, chính sách restart và khả năng tiếp tục chạy sau khi boot. Nếu bạn đang định dùng tmux để chạy một job có tính chất như cron, job đó nên là một service.