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

Chạy Claude Code trên VPS từ xa với tmux

Giữ Claude Code chạy sau khi SSH rớt hoặc đóng nắp laptop bằng tmux trên VPS Linux luôn bật. Xem cách cài đặt, harden và xử lý lỗi thường gặp.

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

Claude Code chạy bình thường trên laptop cho đến khi bạn đóng nắp: phiên SSH bị ngắt, shell nhận SIGHUP, rồi agent đang chạy test được 3 phút cũng dừng theo. Hãy chạy CLI trên một máy không bao giờ sleep, bên trong terminal multiplexer có các process không phải là tiến trình 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 cách vận hành một máy chủ để bạn chạy agent liên tục. Nếu bạn không có Linux server có thể để bật liên tục, nội dung này không áp dụng. Đây 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, sshd fork một shell 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ó. Khi mất kết nối, 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 foreground lâu sẽ bị dừng.

tmux đảo ngược quan hệ sở hữu 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 một 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 kết thúc nhưng server, session và agent đang chạy tác vụ vẫn tiếp tục. Kết nối lại, tmux attach, rồi bạn quay lại đúng shell cũ với cùng scrollback. nohup cũng tiếp tục sau khi nhận 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 background. Claude Code là ứng dụng interactive; tmux (hoặc screen) là công cụ phù hợp.

Cấu hình tài nguyên cho máy chủ

CLI là một process Node; bản thân nó không chiếm hết tài nguyên máy. Thứ làm máy đầy tài nguyên là những gì agent chạy thay bạn: một lần build, một test suite đầy đủ, tsc, một language server hoặc một database trong Docker. Hãy cấu hình tài nguyên 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 nó. Swap biến một lần OOM kill đột ngột thành một lần build chạy 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 hãy theo dõi dung lượng disk: repo, node_modules và Docker image có thể tăng rất nhanh. Nếu toolchain chạy vượt ra ngoài container đến các virtual machine đầy đủ, chẳng hạn một KVM guest hoặc một 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

Tạo một user riêng với home riêng, rồi đặt public key của bạn 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, trong lúc password authentication vẫn còn để dự phòng. Nếu nhận lỗi Permission denied (publickey), nguyên nhân thường là owner hoặc mode của thư mục .ssh đó, không phải bản thân key.

Cố ý không đưa agent vào group sudo. Nếu cần system package, bạn sẽ cài package đó. Một quyết định này loại bỏ phần lớn cách mà một shell command chạy nhầm có thể làm hỏng host.

Vệ sinh SSH cho máy chủ luôn chạy

Bật xác thực bằng password trên một máy luôn kết nối Internet công cộng, đồng thời giữ agent và source code, là một rủi ro không đáng chấp nhậ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, vì vậy hãy thêm một file thay vì sửa cấu hình chính:

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

Không thêm cấu hình key exchange vào file đó nếu bạn không có lý do cụ thể. Một dòng KexAlgorithms được chép từ tài liệu hardening cũ thường chỉ ghim các thuật toán cổ điển. Vì vậy, ssh client mới có thể in cảnh báo rằng kết nối không sử dụng key exchange hậu lượng tử.

Hãy kiểm tra cấu hình và reload. Giữ phiên hiện tại mở trong khi kiểm tra một phiên mới từ terminal thứ hai:

sudo sshd -t && sudo systemctl restart ssh

Có một điểm cần 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 áp dụng cho systemctl restart ssh, nhưng thay đổi đối với Port đang listen cũng cần systemctl daemon-reload và restart ssh.socket.

Tiếp theo là firewall. Hãy allow SSH trước khi enable 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 sau khi hiểu rõ tác dụng của nó. Khi đã tắt xác thực bằng password, brute force vốn không thể đăng nhập thành công. fail2ban chỉ giúp các lần thử thất bại không xuất hiện trong journal.

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

Cuối cùng, hãy tự động patch bằng sudo apt install unattended-upgrades và sudo 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ủ và kết thúc mọi session. Hãy tắt tùy chọn này và reboot theo lịch của bạn, khi không có tác vụ nào đang chạy dở. 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, vì vậy hãy thực hiện trong một maintenance window 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 Node phiên bản hiện tại. Package 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 repo có chữ ký (không dùng 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, tuyệt đối không dùng sudo npm -g. Global prefix thuộc root sẽ gây lỗi permission về sau và để lại các file thuộc root trong npm cache. Trước tiên, 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, còn ~/.profile chỉ chạy với login shell. Dùng Node riêng cho từng user thông qua version manager như nvm cũng đạt kết quả tương tự; mục tiêu trong cả hai trường hợp là npm install -g không bao giờ cần sudo. npm vẫn hoạt động bình thường. Hoặc dùng native install script của Anthropic, hiện là cách được tài liệu hóa mặc định. 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. (API key trong environment là cách còn lại.) Dù dùng cách nào, credential đó hiện nằm trên server, dẫn đến phần mà nhiều người thường bỏ qua.

Bàn về phạm vi ảnh hưởng

Một agent có quyền shell chính là một shell. Nó có thể đọc mọi thứ mà user 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à điểm yếu của công cụ, mà là bản chất của nó. Vì vậy, account chạy agent quan trọng hơn bất kỳ setting riêng lẻ nào.

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

Claude Code có flag bỏ qua hoàn toàn các prompt yêu cầu cấp quyền. Trên laptop hoặc một project dùng thử, bạn tự quyết định. Trên server chứa token, flag này loại bỏ lớp bảo vệ cuối cùng giữa một instruction bị hiểu sai và một git push --force. Các prompt bị bỏ qua cũng không phải lựa chọn bật hoặc tắt toàn bộ. Khi chế độ auto sắp trở thành mặc định mới, bạn nên biết server không được theo dõi nên cố định ở permission mode nào. Phần giải thích flag này thực sự thay đổi gì và cách giới hạn agent chạy với flag đó, từ sandbox tích hợp đến một VPS dùng tạm, có trong cách 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 dùng key trên laptop. Hãy hiểu quyền mà cách này cấp: agent forwarding expose socket của SSH agent local 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 còn kết nối. Quyền này rộng hơn nhiều so với việc “cho git pull repository này”.

Thay vào đó, hãy tạo 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 để các commit từ máy chủ có thể nhận diện được:

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 ~/.tmux.conf tối thiểu:

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

4 lệnh này đủ cho 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 nhớ. Lệnh này attach vào session nếu session đã tồn tại và tạo session nếu chưa có. Vì vậy, một lệnh dùng được cho cả việc bắt đầu ngày làm việc và tiếp tụ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 n và Ctrl-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 cũ (q để thoát).

Có một điều cần biết về các session 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 tiêu tốn token vào việc gì trong một session chạy lâu trước khi để một session chạy trong 1 tuần.

Các lỗi thường gặp

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

Pane 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ỏ. Hãy buộc các client khác detach khi attach: tmux attach -d -t claude.

Build in ra Killed. Chỉ có một dòng, 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 process lớn nhất. Với Node, bạn có thể thấy FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory thay vào đó. Cách xử lý theo thứ tự: thêm swap (như phần trên), giới hạn mức độ chạy 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 bị mất; 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 đã global install vào một prefix do root sở hữu. Dùng prefix ~/.npm-global ở phần trên. Nếu trước đó bạn từng 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 khi. Export PATH của bạn nằm trong ~/.bashrc, bên dưới guard "Nếu không chạy interactive thì không làm gì", nên các shell không interactive sẽ bỏ qua nó. Di chuyển export lên phía trên guard đó và giữ nó trong ~/.bashrc, không phải ~/.profile: tmux có thể khởi động các shell không login. Các shell này đọc ~/.bashrc và không bao giờ đọc ~/.profile.

Màu bị lỗi sau khi attach. Có mismatch ở TERM. Dòng default-terminal ở trên là cách sửa.

Session biến mất sau reboot. Đây không phải bug: tmux server là một process và reboot sẽ kết thúc process đó. 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 đó; khi đó tmux ls 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 chạy đồng thời nhiều session, 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, hữu ích khi agent đang thực hiện một lần refactor dài và muốn agent thứ hai chạy test. Port cũng nhanh chóng trở nên lộn xộn như vậy. Khi 6 repo cùng muốn dùng :3000, đã đến lúc không còn gán port thủ công nữa mà để Traefik reverse proxy định tuyến nhiều app trong Docker Compose bằng hostname.

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

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

Một lưu ý cuối: hãy bind dev server do agent khởi chạy 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 preview, hãy đặt WireGuard VPN tự host trên VPS phía trước chúng: dev server bind vào private interface, còn ufw tiếp tục từ chối mọi kết nối từ public interface. 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 chạy nó bên trong tmux. Một process được khởi chạy trực tiếp từ SSH shell là child của shell đó và sẽ chết cùng pty khi kết nối bị ngắt. Bên trong tmux, shell thuộc về tmux server đã detached, nên agent vẫn tiếp tục chạy giữa chừng và tmux attach đưa bạn trở lại đúng scrollback trước đó. Hãy chạy tmux new -A -s <project> làm command đầu tiên sau mỗi lần login để 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 ra lỗi EACCES ở các 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ó quyền root 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 đ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. Forwarding expose socket của SSH agent cục bộ cho mọi process chạy dưới user đó. Vì vậy, bất kỳ thứ gì trên máy cũng có thể yêu cầu key của bạn ký cho bất kỳ host nào mà nó có thể kết nối đến, trong suốt thời gian bạ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 nếu agent thực sự phải push.

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

Một từ duy nhất mà 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 vào đó. Thực hiện các cách khắc phục theo thứ tự: thêm swapfile, giới hạn mức parallelism 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ị ngắt.

tmux hay systemd service?

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