اجرای کلود کد روی VPS با tmux بدون قطعی SSH
با tmux نشستهای کلود کد را روی VPS لینوکسی همیشه روشن نگه دارید تا با قطع SSH از بین نروند. نصب، امنسازی اولیه و خطاهای رایج مثل SIGHUP را بشناسید.
مشکل از درِ لپتاپ است، نه CLI
Claude Code روی لپتاپ شما بهخوبی کار میکند تا زمانی که درِ آن را ببندید: نشست SSH قطع میشود، شل سیگنال SIGHUP دریافت میکند و agent که سه دقیقه از اجرای تست آن گذشته است، همراه با آن از بین میرود. CLI را روی ماشینی اجرا کنید که هرگز به حالت خواب نمیرود، درون یک تسهیمکنندهٔ ترمینال که فرآیندهایش فرزند نشست SSH شما نیستند. تمام ترفند همین است و tmux، نه فرایند نصب، بخش اصلی ماجرا است.
این صفحه دربارهٔ کار با دستگاهی است که agentها را روی آن در حال اجرا رها میکنید. اگر سرور لینوکسی ندارید که بتوانید آن را روشن رها کنید، هیچیک از این موارد کاربرد ندارد. این تنها پیشنیاز صادقانه است.
tmux واقعاً چه کاری انجام میدهد
وقتی با SSH وارد میشوید، sshd یک شل ایجاد میکند و یک شبهپایانه به آن میدهد؛ هر چیزی که از آن شل شروع کنید، فرزند آن است. اگر اتصال قطع شود، هسته شبهپایانه را از بین میبرد، شل سیگنال SIGHUP دریافت میکند و به نوبهٔ خود فرزندانش را نیز قطع میکند. فرایندهای طولانیمدت پیشزمینه از بین میروند.
tmux مالکیت را وارونه میکند. دستور tmux که تایپ میکنید یک کلاینت سبک است که از طریق یک سوکت یونیکس با یک سرور tmux صحبت میکند که جدا از ترمینال شما اجرا میشود. شلهای درون یک نشست، فرزندان آن سرور هستند، نه sshd. اتصال SSH را قطع کنید، کلاینت از بین میرود در حالی که سرور، نشست و عامل در حال انجام وظیفه به اجرا ادامه میدهند. دوباره متصل شوید، tmux attach، و به همان شل با همان تاریخچهٔ اسکرول بازمیگردید. nohup نیز از قطع اتصال جان سالم به در میبرد، اما راهی برای بازگشت به شما نمیدهد؛ نمیتوانید به یک رابط کاربری متنی که به پسزمینه رفته متصل شوید. Claude Code تعاملی است؛ tmux (یا screen) ابزار درست است.
اندازهگیری منابع مورد نیاز
CLI یک فرآیند Node است؛ این CLI نیست که ماشین را پر میکند. آنچه ماشین را پر میکند هر چیزی است که agent از طرف شما اجرا میکند: یک build، یک مجموعه تست کامل، tsc, یک language server، یک پایگاه داده در Docker. منابع را برای زنجیره ابزار اندازه بگیرید، نه برای CLI. حتی اگر برنامه ندارید از آن استفاده کنید، swap اضافه کنید، چرا که یک kill سخت OOM را به یک build کُند تبدیل میکند:
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همچنین مراقب دیسک باشید: مخازن، node_modules و ایمیجهای Docker به سرعت انباشته میشوند. و اگر زنجیره ابزار از کانتینرها فراتر رفته و به ماشینهای مجازی کامل، یک مهمان KVM، یا یک گره محلی Kubernetes برسد، پیش از آنکه متعهد شوید بررسی کنید که طرح انتخابی، افزونههای مجازیسازی CPU را در معرض دید قرار میدهد، زیرا اجرای مجازیسازی تودرتو روی یک VPS چیزی است که ارائهدهنده برای شما فعال میکند، نه چیزی که شما از درون مهمان روشن کنید.
ابتدا یک کاربر غیر ریشه
یک کاربر اختصاصی با دایرکتوری خانگی خودش بسازید و کلید عمومیتان را در جای مناسب قرار دهید:
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عمداً، agent در گروه sudo قرار ندارد. اگر بستهای سیستمی لازم باشد، شما آن را نصب میکنید. همین یک تصمیم، بیشتر راههایی را که یک فرمان اشتباه شل میتواند میزبان را خراب کند، از بین میبرد.
بهداشت SSH برای دستگاهی که روشن نگه میدارید
احراز هویت با گذرواژه روی دستگاهی که تمام روز در اینترنت عمومی قرار دارد، یک عامل و کد منبع شما را نگه میدارد، ریسکی نیست که ارزش پذیرش داشته باشد. آن را خاموش کنید. در اوبونتو 24.04 و دبیان 13، /etc/ssh/sshd_config شامل /etc/ssh/sshd_config.d/*.conf میشود، بنابراین به جای ویرایش پیکربندی اصلی، یک فایل در آن قرار دهید:
# /etc/ssh/sshd_config.d/10-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin noاعتبارسنجی و بارگذاری مجدد کنید، در حالی که نشست فعلی خود را باز نگه میدارید و یک نشست جدید را از ترمینال دوم آزمایش میکنید:
sudo sshd -t && sudo systemctl restart sshیک نکته ظریف در اوبونتو 24.04: sshd با سوکت فعال میشود. تنظیمات احراز هویت در systemctl restart ssh اعمال میشوند، اما تغییر در Port شنود به systemctl daemon-reload و راهاندازی مجدد ssh.socket نیز نیاز دارد.
سپس دیوار آتش. پیش از فعالسازی آن، SSH را مجاز کنید، وگرنه دسترسی خود را قطع میکنید:
sudo ufw allow OpenSSH
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enablefail2ban را با درک روشنی از آنچه برایتان فراهم میکند نصب کنید: وقتی احراز هویت با گذرواژه خاموش باشد، حمله جستجوی فراگیر به هر حال موفق نمیشود، تلاشهای ناموفق را از گزارش شما دور نگه میدارد.
# /etc/fail2ban/jail.local
[sshd]
enabled = true
backend = systemd
maxretry = 5
bantime = 1hدر نهایت، با sudo apt install unattended-upgrades و sudo dpkg-reconfigure -plow unattended-upgrades بهطور خودکار وصله کنید. به تعامل با tmux توجه کنید: Unattended-Upgrade::Automatic-Reboot را روشن کنید و یک بهروزرسانی هسته دستگاه را راهاندازی مجدد میکند و تمام نشستها را با خود میبرد. آن را خاموش بگذارید و در زمانبندی خودتان، وقتی هیچ فرایندی در حال اجرا نیست، راهاندازی مجدد کنید.
نصب Node.js و Claude Code روی اوبونتو
Claude Code یک CLI مبتنی بر Node است، بنابراین به یک نسخهٔ جاری از Node نیاز دارید. بستهٔ توزیع معمولاً قدیمی است؛ NodeSource مسیر معمول روی اوبونتو و دبیان است و یک مخزن امضاشده ارائه میدهد (بدون apt-key، آن ابزار منسوخ شده است):
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
node --versionحالا بخشی که افراد اشتباه متوجه میشوند: CLI را بهعنوان کاربر agent خود نصب کنید، هرگز با sudo npm -g. یک پیشوند سراسری تحت مالکیت root بعداً خطاهای دسترسی ایجاد میکند و فایلهای تحت مالکیت root در کش npm باقی میگذارد. ابتدا پیشوند npm را به خانهٔ کاربر اشاره دهید:
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این export در ~/.bashrc قرار میگیرد، نه ~/.profile، و باید بالاتر از محافظ «اگر تعاملی اجرا نمیشود، هیچ کاری نکن» نزدیک ابتدای فایل قرار گیرد: tmux ممکن است پوستههای غیر-ورودی راهاندازی کند که ~/.bashrc را میخوانند و ~/.profile را رد میکنند، ~/.profile تنها برای پوستههای ورودی اجرا میشود. یک Node متعلق به کاربر از طریق یک مدیر نسخه مانند nvm همان هدف را تأمین میکند؛ هدف در هر دو حالت این است که npm install -g هرگز به sudo نیاز نداشته باشد. npm همچنان بهخوبی کار میکند، یا از اسکریپت نصب اصلی Anthropic استفاده کنید، که پیشفرض مستندشدهٔ فعلی است. پیش از جایگذاری، مستندات نصب Anthropic را بررسی کنید، روشهای نصب تغییر میکنند.
claude را درون یک مخزن اجرا کنید تا آن را راهاندازی کنید. اولین اجرا شما را از طریق احراز هویت راهنمایی میکند؛ یک ماشین بدون واسط کاربری مرورگر ندارد، بنابراین فرآیند یک URL به شما میدهد تا روی ماشین خودتان باز کنید و یک کد برای بازگرداندن به ترمینال. (یک کلید API در محیط، مسیر دیگر است.) در هر دو حالت، آن اعتبارنامه اکنون روی سرور قرار میگیرد، که ما را به بخشی میرساند که افراد از آن صرفنظر میکنند.
گفتوگو دربارهٔ شعاع تخریب
عاملی که به پوسته دسترسی دارد، خود یک پوسته است. میتواند هر چیزی را که کاربری که تحت آن اجرا میشود میتواند بخواند، بخواند و به هر جایی که آن کاربر میتواند ارسال کند، ارسال کند. این نقد ابزار نیست، تعریف آن است و به همین دلیل است که حساب کاربریای که تحت آن اجرا میشود، از هر تنظیم جداگانهای مهمتر است.
- کاربر اختصاصی و بدون امتیاز. بدون گروه
sudo, بدون دایرکتوری خانگی مشترک با حساب خودتان. - هیچ اعتبارنامهٔ تولیدی روی ماشین نباشد. بدون
~/.aws/credentialsکه کلیدهای تولید را نگه دارد، بدون.envکه از تولید کپی شده باشد، بدون رمز عبور پایگاه داده با دسترسی نوشتن به هر چیزی که اهمیت دارد. به عامل یک اعتبارنامهٔ مرحلهبندی یا فقط خواندنی بدهید. - توکنهای محدودشده. یک توکن گیتهاب با دسترسی ریزدانه که به یک مخزن محدود است؛ یک کلید استقرار وقتی دسترسی خواندن کافی است.
Claude Code پرچمی ارائه میدهد که درخواستهای مجوز را بهکلی رد میکند. روی یک لپتاپ، روی یک پروژهٔ موقت، این تصمیم با شماست. روی سروری که توکن نگه میدارد، این پرچم آخرین چیزی را که میان یک دستور اشتباه خواندهشده و یک git push --force قرار دارد حذف میکند. این که پرچم واقعاً چه چیزی را تغییر میدهد و چگونه عاملی که با آن اجرا میشود را مهار کنیم، از جعبهٔ شنی داخلی تا یک VPS یکبارمصرف، در اجرای امن Claude Code روی سرور پوشش داده شده است.
کلید استقرار در برابر ارسال عامل SSH
وسوسهانگیز است که ssh -A کنید تا گیت بتواند از کلید روی لپتاپ شما استفاده کند. بفهمید که این کار چه چیزی اعطا میکند: ارسال عامل، سوکت عامل SSH محلی شما را در معرض فرایندهایی قرار میدهد که بهعنوان آن کاربر روی ماشین اجرا میشوند. هر چیزی که بهعنوان agent اجرا شود، از جمله عامل، میتواند از کلید شما بخواهد که برای هر میزبانی که میتواند به آن دسترسی پیدا کند امضا کند، تا زمانی که شما متصل بمانید. این بسیار فراتر از «اجازه بده گیت این یک مخزن را pull کند» است.
در عوض، یک کلید روی سرور تولید کنید، آن را بهعنوان یک کلید استقرار برای هر مخزن ثبت کنید (دسترسی نوشتن فقط اگر عامل نیاز به push داشته باشد)، و یک هویت گیت تنظیم کنید تا commitهای انجامشده از روی ماشین قابل شناسایی باشند:
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"گردش کار tmux
ابتدا آن را نصب کنید (sudo apt install tmux)، سپس یک فایل ~/.tmux.conf کمینه بسازید:
set -g mouse on
set -g history-limit 50000
set -g default-terminal "tmux-256color"چهار فرمان کار روزمره را پوشش میدهند:
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 claudetmux new -A -s claude فرمانی است که باید به خاطر بسپارید؛ اگر نشست وجود داشته باشد متصل میشود و اگر وجود نداشته باشد آن را میسازد، بنابراین یک فرمان هم شروع روز و هم ازسرگیری پس از قطع ارتباط را پوشش میدهد. برای آن یک میانبر تعریف کنید. درون یک نشست، Ctrl-b c یک پنجره باز میکند، Ctrl-b n و Ctrl-b p بین آنها حرکت میکنند، و Ctrl-b [ برای پیمایش به عقب وارد حالت کپی میشود (q از آن خارج میشود).
یک نکته درباره نشستهایی که هرگز نمیکشید: عامل در هر نوبت کل مکالمه را دوباره ارسال میکند، بنابراین پیش از آنکه یک نشست claude-code-token-usage-explained را برای یک هفته روشن رها کنید، بخوانید یک نشست طولانی مدت Claude Code توکنهایش را صرف چه میکند.
حالتهای خرابی
«نشست من از بین رفته است.» tmux ls چاپ میکند no server running on /tmp/tmux-1000/default. این تقریباً همیشه یعنی فرایند هرگز داخل tmux نبوده، شما با SSH وارد شدهاید، claude را مستقیماً اجرا کردهاید و قطع ارتباط آن را کشته است. چیزی برای بازیابی نیست. عادتی که از آن جلوگیری میکند: tmux new -A -s <project> اولین دستور پس از هر ورود باشد.
پنل به یک جعبهٔ کوچک کوچک میشود. tmux اندازهٔ نشست را به کوچکترین کلاینت متصل تنظیم میکند، بنابراین یک کلاینت کهنه که هنوز از ماشین دیگری متصل است نمایشگر را فشرده میکند. هنگام اتصال، بقیه را به زور بیرون کنید: tmux attach -d -t claude.
یک بیلد چاپ میکند Killed. یک کلمه، بدون ردپای پشته. با sudo dmesg -T | grep -i -E 'out of memory|killed process' تأیید کنید، قاتل OOM هسته بزرگترین فرایند را انتخاب کرده است. از Node ممکن است بهجای آن FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory را ببینید. راهحلها، به ترتیب: افزودن swap (بالا)، محدود کردن موازیسازی تست و کامپایلر، بالا بردن heap نود با NODE_OPTIONS=--max-old-space-size=...، یا ارتقای VPS. قاتل OOM ممکن است سرور tmux را بهجای بیلد انتخاب کند و نشست شما را با خود ببرد؛ اگر systemd-oomd در حال اجرا باشد، میتواند یک slice کاربر کامل را با همان اثر بکشد.
npm error code EACCES / permission denied, mkdir '/usr/lib/node_modules/...'. یک نصب سراسری در یک prefix تحت مالکیت root. از prefix ~/.npm-global در بالا استفاده کنید. اگر قبلاً sudo npm را در نقطهای اجرا کرده باشید ممکن است Your cache folder contains root-owned files را هم ببینید، با sudo chown -R $(id -u):$(id -g) ~/.npm تعمیر کنید.
claude: command not found, اما فقط گاهی. export PATH شما در ~/.bashrc زیر محافظ «اگر به صورت تعاملی اجرا نمیشود، هیچ کاری نکن» قرار دارد، بنابراین پوستههای غیرتعاملی آن را رد میکنند. export را بالای آن محافظ منتقل کنید و آن را در ~/.bashrc نگه دارید، نه ~/.profile: tmux ممکن است پوستههای non-login راهاندازی کند که ~/.bashrc را میخوانند و هرگز به ~/.profile دست نمیزنند.
رنگهای درهم پس از اتصال. یک عدم تطابق TERM, خط default-terminal بالا راهحل است.
نشستها پس از راهاندازی مجدد ناپدید میشوند. یک باگ نیست: سرور tmux یک فرایند است و راهاندازی مجدد آن را پایان میدهد. uptime را بررسی کنید.
چه چیزهایی با رشد این ساختار میشکنند
پروژههای بیشتر. به ازای هر مخزن یک نشست tmux با نام همان مخزن بسازید؛ آنگاه tmux ls داشبورد شما خواهد بود. اگر نظم در نامگذاری را رعایت نکنید، نشستهایی با نامهای 0, 1, 2 خواهید داشت. پورتها نیز به همین شکل پراکنده میشوند؛ وقتی شش مخزن همگی :3000 را بخواهند، همان نقطهای است که باید تخصیص دستی پورت را متوقف کنید و اجازه دهید یک پروکسی معکوس Traefik چند برنامه را تحت Docker Compose مسیریابی کند تا توزیع را بر اساس نام میزبان انجام دهد.
افراد بیشتر. سوکتهای tmux به ازای هر کاربر هستند، بنابراین دو توسعهدهنده روی یک ماشین، هر کدام سرور tmux خود را دارند و نمیتوانند نشستهای یکدیگر را ببینند. به اشتراکگذاشتن یک نشست از طریق یک سوکت مشترک به این معناست که همه به عنوان همان کاربر Unix در همان شل تایپ میکنند، با تمام تبعات حسابرسی و مجوزهایی که این کار دارد. استفاده از کاربران مجزا، پاسخِ خستهکننده اما درست است.
کارهای بدون نظارت. tmux برای نشستهای تعاملی است که به آنها متصل میشوید. کارهایی که طبق زمانبندی و بدون ناظر اجرا میشوند، متعلق به یک unit و timer در systemd هستند، جایی که به صورت رایگان ثبت رویداد، سیاست راهاندازی مجدد و بقا در هنگام بوت را دریافت میکنند. استفاده از tmux برای اجرای کاری شبیه cron نشانهای است از اینکه آن کار میخواهد یک سرویس باشد.
یک نکته آخر: سرورهای توسعهای که agent راهاندازی میکند را به 127.0.0.1 متصل کنید، نه 0.0.0.0، و از طریق یک تونل SSH (ssh -L 3000:127.0.0.1:3000 agent@your-server) به آنها دسترسی پیدا کنید، به جای باز کردن پورتها در ufw. زمانی که نیم دوجین پورت را forward میکنید، یا یک تلفن همراه و یک لپتاپ هر دو همزمان پیشنمایش را میخواهند، یک VPN وایرگارد خودمیزبان روی VPS را جلوی آنها قرار دهید: سرورهای توسعه به یک رابط خصوصی متصل میشوند و ufw همچنان همه چیز را از رابط عمومی رد میکند. فایروال فقط زمانی کمک میکند که از ایجاد حفره در آن دست بردارید.
Claude Code تنها انتخاب نیست: اجرای یک عامل کدنویسی هوش مصنوعی روی VPS گزینههای Aider و Goose را نیز بررسی میکند.
FAQ
آیا Claude Code پس از قطع شدن اتصال SSH من همچنان به اجرا ادامه میدهد؟
تنها در صورتی که آن را درون tmux راهاندازی کرده باشید. فرآیندی که مستقیماً از پوسته SSH اجرا میشود، فرزند آن پوسته است و با قطع شدن اتصال، همراه با pty از بین میرود. درون tmux، پوسته متعلق به سرور tmux جداشده است، بنابراین عامل در میانه کار به اجرا ادامه میدهد و tmux attach شما را به همان بافر اسکرولبک بازمیگرداند. tmux new -A -s <project> را به اولین دستور پس از هر ورود تبدیل کنید تا مشکل برطرف شود.
آیا باید CLI را با sudo npm install -g نصب کنم؟
خیر. یک پیشوند سراسری تحت مالکیت root در نصبهای بعدی خطاهای EACCES و فایلهای تحت مالکیت root در کش npm را برایتان به همراه دارد. پیشوند npm را روی ~/.npm-global تنظیم کنید (یا از یک مدیر نسخه مانند nvm استفاده کنید)، بهعنوان کاربر غیرممتاز agent نصب کنید و ~/.npm-global/bin را از ~/.bashrc به PATH صادر کنید، بالاتر از محافظ تعاملی. اگر پیشتر یک بار sudo npm را اجرا کردهاید، کش را با sudo chown -R $(id -u):$(id -g) ~/.npm تعمیر کنید.
آیا ارسال عامل ssh -A روی دستگاهی که یک عامل اجرا میکند امن است؟
این کار دسترسی بسیار فراتر از نیاز کار را اعطا میکند. ارسال، سوکت عامل SSH محلی شما را در معرض تمام فرآیندهایی که بهعنوان آن کاربر اجرا میشوند قرار میدهد، بنابراین هر چیزی روی آن دستگاه میتواند از کلید شما بخواهد برای هر میزبانی که میتواند به آن دسترسی پیدا کند، تا زمانی که شما متصل هستید، امضا کند. یک کلید ed25519 روی سرور تولید کنید و آن را بهعنوان یک کلید استقرار مختص هر مخزن ثبت کنید، تنها در صورتی که عامل واقعاً نیاز به push کردن داشته باشد، با دسترسی نوشتن.
چرا ساخت من فقط Killed را چاپ میکند؟
یک کلمه بدون هیچ ردپایی از پشته، قاتل OOM هسته است. آن را با sudo dmesg -T | grep -i -E 'out of memory|killed process' تأیید کنید؛ از Node ممکن است بهجای آن JavaScript heap out of memory را ببینید. راهحلها را به ترتیب اعمال کنید: یک فایل swap اضافه کنید، موازیسازی تست و کامپایلر را محدود کنید، NODE_OPTIONS=--max-old-space-size=... را افزایش دهید، سپس VPS را ارتقا دهید. مراقب باشید که قاتل OOM میتواند سرور tmux را بهجای فرآیند ساخت انتخاب کند و کل نشست شما را با خود ببرد.
tmux یا یک سرویس systemd؟
tmux مناسب نشستهای تعاملی است که به آنها متصل میشوید، تماشا میکنید و در آنها تایپ میکنید، که دقیقاً همان چیزی است که یک نشست عامل میباشد. کاری که بر اساس یک زمانبندی و بدون ناظر اجرا میشود، به یک unit و timer systemd تعلق دارد، جایی که ثبت رویدادها، یک سیاست راهاندازی مجدد و بقا در هنگام بوت بهصورت رایگان فراهم میشوند. اگر برای اجرای یک کار cron-مانند به سراغ tmux میروید، آن کار باید یک سرویس باشد.