SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor

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 máy

Bản thân CLI chỉ là một binary native duy nhất; nó không phải thứ làm đầy máy. Thứ làm đầy máy là bất cứ gì agent chạy thay bạn: một bản build, toàn bộ test suite, tsc, một language server hoặc một 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 bị OOM kill nghiêm trọng thành một bả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

Đồng thời hãy theo dõi disk: repo, node_modules và Docker image có thể tăng rất nhanh. Nếu toolchain vượt ra ngoài container để chạy cả virtual machine, KVM guest hoặc một node Kubernetes 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 phải bật cho bạn, không phải tùy chọn 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 Node.js và Claude Code trên Ubuntu

Cách cài qua npm cần Node.js 22 trở lên. (Gói này hiện kéo theo một native binary không chạy trên Node, nhưng npm vẫn kiểm tra phiên bản engine; ngoài ra, toolchain của agent thường cũng cần Node phiên bản mới. ) Gói của distro thường chậm cập nhật; trên Ubuntu và Debian, NodeSource là lựa chọn phổ biến và cung cấp signed repo (không dùng apt-key, vì thành phần này đã deprecated trên Ubuntu 24.04 và đã bị loại bỏ khỏi Debian 13):

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, 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, phía trên guard “If not running interactively, don't do anything” gần đầu file. Một tmux pane mặc định chạy login shell: tmux để default-command trống, khiến nó khởi động default-shell dưới dạng login shell; login bash sẽ đọc ~/.profile, và trên Ubuntu file này source ~/.bashrc. Non-login shell, chẳng hạn khi bạn tự đặt default-command, sẽ đọc trực tiếp ~/.bashrc. Vì vậy, ~/.bashrc bao phủ cả hai trường hợp, còn chỉ dùng ~/.profile sẽ bỏ sót trường hợp thứ hai. Có một điểm cần lưu ý: bash bỏ qua ~/.profile nếu ~/.bash_profile hoặc ~/.bash_login tồn tại. Vì vậy, nếu bạn đã tạo một trong các file đó, hãy cho file đó source cả ~/.bashrc. Dùng version manager như nvm để cài Node cho từng user cũng đạt kết quả tương tự; 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, nhưng tài liệu của Anthropic đánh dấu native install script là cách được khuyến nghị. Các signed repository cho apt, dnf và apk cũng là một lựa chọn khác. Hãy kiểm tra tài liệu cài đặt của Anthropic trước khi paste 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. (Dùng API key trong environment là lựa chọn khác.) Dù dùng cách nào, credential đó hiện được lưu trên server. Đây là phần nhiều người bỏ qua.

Trao đổi về phạm vi ảnh hưởng

Agent có quyền truy cập shell thực chất là một shell. Nó có thể đọc mọi thứ mà user chạy agent 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 tool 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 nâng cao. Không thuộc group sudo và không dùng chung home directory với account của bạn.
  • Không đặt credential production trên máy. Không có ~/.aws/credentials chứa production key, không copy .env từ production xuống máy và không dùng database password có quyền ghi vào bất kỳ tài nguyên 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 có quyền chi tiết và chỉ giới hạn trong một repository; dùng deploy key khi chỉ cần quyền đọc.

Claude Code có flag --dangerously-skip-permissions để bỏ qua gần như toàn bộ permission prompt. Trên laptop hoặc project dùng thử, bạn có thể 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 chỉ dẫn bị đọc sai và một git push --force. Các prompt bị bỏ qua cũng không chỉ có hai trạng thái bật hoặc tắt. Với auto mode hiện là mode khởi động mặc định (từ Claude Code v2.1.283), bạn nên biết server không được theo dõi trực tiếp cần được cố định ở permission mode nào. Phần chạy Claude Code an toàn trên server 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 sẵn đến một VPS có thể hủy bỏ.

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 duy trì kết nối. Quyền này lớn hơn rất nhiều so với việc "cho phép git pull một repo duy nhất".

Thay vào đó, hãy tạo key trên server, đăng ký key này 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 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 đáp ứng các thao tác 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. Bên 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 trước đó (q để thoát). Nếu bạn gõ prompt bằng Telex qua Unikey hoặc EVKey, bộ gõ vẫn chạy trên laptop chứ không chạy trên server, nên lỗi mất dấu hoặc lặp chữ vẫn có thể đi theo bạn qua SSH và tmux; hướng dẫn sửa lỗi gõ tiếng Việt trong Claude Code giải thích nguyên nhân và cách dùng Ctrl+G để soạn prompt trong editor.

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ộ cuộc hội thoại trong mỗi lượt, vì vậy hãy đọc Claude Code dùng token cho những 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 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ư luôn có nghĩa là tiến trình 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 tiến trình bị dừng. Không có gì để khôi phục. Thói quen giúp tránh 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 thu nhỏ thành một ô rất nhỏ. Tùy chọn window-size quyết định client đang attach nào được dùng để đặt kích thước. Từ tmux 3.1 (Ubuntu 24.04 đi kèm phiên bản 3.4), mặc định là latest, tức client có hoạt động gần đây nhất. Tuy nhiên, cấu hình có window-size smallest sẽ đặt kích thước 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 bị ngắt khi attach bằng: tmux attach -d -t claude.

Một build in ra Killed. Chỉ có một từ, 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 sử dụng nhiều tài nguyê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. 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 bị mất theo. Nếu systemd-oomd đang chạy, nó cũng có thể dừng 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/...'. Đây là lỗi cài đặt toàn cục vào prefix thuộc root. Sử dụng prefix ~/.npm-global như 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 khi. Dòng export PATH của bạn nằm trong ~/.bashrc, bên dưới guard "If not running interactively, don't do anything", nên các shell không tương tác sẽ bỏ qua nó. Chuyển dòng export lên trên guard đó và giữ nó trong ~/.bashrc. Một pane tmux mặc định là login shell và đọc đến ~/.bashrc thông qua ~/.profile. Mọi non-login shell đọc trực tiếp ~/.bashrc. Vì vậy, một file này bao phủ cả hai trường hợp.

Màu 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 lỗi: 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 đó; 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.