نصب GitHub Actions runner روی VPS با Ubuntu 24.04
راهنمای ثبت GitHub Actions runner روی Ubuntu 24.04 با کاربر اختصاصی، بررسی checksum، اجرای config.sh، سرویس systemd و ریسک pull requestهای fork.
یک runner خودمیزبان GitHub Actions چه کاری انجام میدهد
یک runner خودمیزبان GitHub Actions برنامهای است که آن را روی VPS خودتان نصب میکنید. این برنامه برای دریافت jobها از GitHub درخواست میفرستد و آنها را روی سختافزار شما اجرا میکند. runner را به یک repository ثبت میکنید، آن را بهصورت یک systemd service نصب میکنید و پس از هر reboot دوباره اجرا میشود. GitHub زمانبندی job را انجام میدهد. سرور شما کار را اجرا میکند.
استفاده از CI (continuous integration) روی سیستمی که مالک آن هستید، به دو دلیل ارزشمند است. دیگر زمان build بر اساس میزان مصرف محاسبه نمیشود. همچنین job میتواند به منابعی دسترسی پیدا کند که فقط روی سیستم شما وجود دارند، مانند build cache آماده یا یک private network. هزینه این کار، امنیت است. runner هر چیزی را که فایل workflow مشخص کند، با حساب کاربریای که برای آن تعیین کردهاید اجرا میکند. بنابراین فایل workflow عمداً امکان اجرای کد از راه دور را فراهم میکند. در یک private repository این مسئله قابلقبول است، زیرا فقط افرادی که به آنها اعتماد دارید میتوانند چنین فایلی اضافه کنند. در یک public repository، این موضوع یک خطر واقعی است. بخش مربوط به pull requestهای fork سازوکار آن را توضیح میدهد.
تمام مطالب زیر درباره Ubuntu 24.04 و runner با نسخه 2.336.0 است؛ این نسخه در July 2026، نسخه فعلی محسوب میشود.
پیشنیازهای شروع
کار را با یک VPS شروع کنید که یک حساب کاربری معمولی مدیر و دسترسی sudo داشته باشد؛ یعنی همان وضعیتی که در ده دقیقه اول راهاندازی یک VPS جدید به آن میرسید. نیازی به باز کردن port ورودی ندارید. runner یک اتصال خروجی HTTPS (پروتکل انتقال ابرمتن امن) به GitHub باز میکند و هنگام انتظار برای کار، آن را باز نگه میدارد؛ بنابراین GitHub هرگز به سرور شما متصل نمیشود. فایروال شما میتواند به روی شبکه جهانی بسته بماند و jobها همچنان دریافت شوند.
همچنین باید در repository دسترسی مدیر داشته باشید، زیرا registration token در تنظیمات repository نمایش داده میشود.
ایجاد یک کاربر اختصاصی برای runner
هرگز runner را با حساب root یا حساب مدیریتی خود اجرا نکنید. هر job مجوزهای کاربر runner را به ارث میبرد؛ بنابراین workflowای که sudo را اجرا میکند، در صورتی موفق میشود که کاربر runner بتواند از sudo استفاده کند. یک کاربر غیرممتاز ایجاد کنید که بهجز شاخه home خودش، مالک هیچ چیز دیگری نباشد. حسابهای کاربری با کمترین سطح مجوز در VPS الگوی کلی را توضیح میدهد. نمونه اختصاصی در ادامه آمده است.
sudo useradd -m -s /bin/bash gharunner
sudo passwd -l gharunner
sudo chmod 750 /home/gharunner
sudo install -d -m 700 -o gharunner -g gharunner /home/gharunner/actions-runnerpasswd -l گذرواژه را قفل میکند؛ بنابراین هیچکس نمیتواند با استفاده از گذرواژه بهعنوان gharunner وارد شود. حالت 700 برای شاخه runner اهمیت دارد، زیرا runner اطلاعات اعتباری خود را در آنجا بهصورت متن ساده ذخیره میکند و یک checkout ممکن است حاوی کد منبع خصوصی باشد.
پیش از ادامه، هر دو ویژگی را بررسی کنید:
sudo passwd -S gharunner
sudo -l -U gharunnerpasswd -S خطی را چاپ میکند که با gharunner L شروع میشود؛ در این خروجی L به این معناست که گذرواژه قفل است. sudo -l -U gharunner باید با is not allowed to run sudo پاسخ دهد. اگر بهجای آن فهرستی از فرمانهای مجاز چاپ شود، حساب عضو یک گروه sudo است و جداسازیای که ایجاد کردید دیگر برقرار نیست.
دریافت runner و بررسی tarball
از اینجا به بعد، کاربر runner باشید.
sudo -iu gharunner
cd ~/actions-runner
RUNNER_VERSION=2.336.0
curl -fL -o actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
"https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"اگر از معماری مطمئن نیستید، ابتدا uname -m را اجرا کنید. x86_64 فایل linux-x64 بالا را دریافت میکند. aarch64 نیز actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz را دریافت میکند.
اکنون آنچه را دریافت کردهاید بررسی کنید. مقدار SHA256 (الگوریتم هش امن، 256 بیتی) زیر مربوط به tarball نسخه 2.336.0 برای x64 است. GitHub مقدار مربوط به release فعلی را در صفحه release و صفحه New self-hosted runner نمایش میدهد. این مقدار با هر نسخه تغییر میکند؛ بنابراین هنگام نصب نسخهای دیگر، آن را از همان صفحه کپی کنید.
echo "04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d actions-runner-linux-x64-2.336.0.tar.gz" | sha256sum -cدریافت موفق یک خط خروجی تولید میکند:
actions-runner-linux-x64-2.336.0.tar.gz: OKفایل ناقص یا تغییریافته، پیام شکست و یک هشدار نمایش میدهد:
actions-runner-linux-x64-2.336.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT matchاین بررسی را نادیده نگیرید و اجازه ندهید tar مشکل را پیدا کند. یک archive که بهطور ناقص نوشته شده است، با gzip: stdin: unexpected end of file و tar: Unexpected EOF in archive شکست میخورد. این پیامها نشان میدهند فایل خراب است، اما مشخص نمیکنند که فایل ناقص دریافت شده یا جایگزین شده است.
tar xzf ./actions-runner-linux-x64-2.336.0.tar.gz
lstarball شامل چه چیزهایی است و چه چیزهایی نیست
پس از استخراج، شاخه شامل config.sh، run.sh، env.sh، safe_sleep.sh، bin/ و externals/ است. bin/ شامل binaryهای runner و bin/installdependencies.sh است. externals/ شامل Node runtime همراه است که actionهای JavaScript روی آن اجرا میشوند.
هنوز svc.sh وجود ندارد. مستندات GitHub آن را scriptی توصیف میکنند که «پس از افزودن موفق runner ایجاد میشود»، زیرا این script از یک template ساخته میشود و نام repository و runner شما در نام service آن درج میشود. بنابراین اجرای sudo ./svc.sh install پیش از ./config.sh با sudo: ./svc.sh: command not found شکست میخورد. ابتدا runner را register کنید، سپس service را نصب کنید.
نصب وابستگیهای runner
runner یک برنامه .NET است و به چند کتابخانهٔ اشتراکی نیاز دارد. shell کاربر runner را حفظ کنید و کتابخانهها را با sudo نصب کنید، زیرا script در پایگاهدادهٔ بستههای سیستم مینویسد.
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.shدر Ubuntu 24.04، این دستور libkrb5-3، zlib1g، liblttng-ust1t64، libssl3t64 و libicu74 را نصب میکند. script برای هر کتابخانه چند نام نسخه را امتحان میکند و نامی را که release شما ارائه میکند نگه میدارد. به همین دلیل، همین script روی نسخههای قدیمیتر Ubuntu و Debian نیز کار میکند.
اگر این مرحله را رد کنید، ./config.sh پیش از انجام هر کاری متوقف میشود:
Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.نبودن libicu همین راهنمایی را با خط اول متفاوتی، یعنی Libicu's dependencies is missing for Dotnet Core 6.0، نمایش میدهد. هر دو خطا از یک علت ناشی میشوند: config.sh پیش از شروع، ldd را روی کتابخانههای همراه اجرا میکند. بنابراین، link حلنشده باعث توقف script میشود و از ایجاد crash نامفهوم در ادامه جلوگیری میکند.
runner را در repository ثبت کنید
یک token از repository دریافت کنید. ابتدا Settings، سپس Actions، بعد Runners و در پایان New self-hosted runner را باز کنید. صفحه یک registration token نشان میدهد که با A شروع میشود. این token یک ساعت پس از ایجاد منقضی میشود؛ بنابراین آن را زمانی تولید کنید که آماده paste کردن آن هستید.
با کاربر runner ثبتنام کنید. config.sh با sudo اجرا نمیشود.
sudo -iu gharunner
cd ~/actions-runner
./config.sh --url https://github.com/YOUR-USER/YOUR-REPO \
--token PASTE_REGISTRATION_TOKEN_HERE \
--name vps-runner-1 \
--labels vps \
--work _work \
--unattended \
--replaceکاربرد این flagها. --name نامی است که runner با آن در repository نمایش داده میشود؛ بنابراین نامی انتخاب کنید که پس از 6 ماه نیز آن را تشخیص دهید. --labels برچسبهای دلخواه شما را اضافه میکند؛ runner بدون نیاز به درخواست، از قبل برچسبهای self-hosted، Linux و X64 را دارد. --work نام directory محل قرارگیری checkoutها را درون directory مربوط به runner تعیین میکند. --unattended به promptهای تعاملی با مقدارهای پیشفرض آنها پاسخ میدهد؛ این همان چیزی است که هنگام قرار دادن command در یک script لازم دارید. --replace registration موجودی را که همین نام را دارد جایگزین میکند، نه اینکه با خطا متوقف شود؛ این رفتار هنگام بازسازی server مطلوب است.
اجرای موفق با این خطها پایان مییابد:
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.registration اکنون در directory مربوط به runner و بهصورت .runner، .credentials و .credentials_rsaparams قرار دارد. 2 مورد آخر این runner را برای GitHub شناسایی میکنند؛ بنابراین هرکس بتواند آنها را بخواند، میتواند خود را بهجای آن معرفی کند. به همین دلیل mode این directory برابر 700 است و کاربر دسترسی sudo ندارد.
نصب runner بهعنوان یک سرویس systemd
اجرای ./run.sh در یک ترمینال برای یک آزمایش مناسب است، اما با پایان جلسه SSH متوقف میشود. سرویس را نصب کنید تا runner هنگام راهاندازی سیستم شروع شود. سرویسها و timerهای systemd در یک VPS خود فایلهای unit را توضیح میدهد. در اینجا svc.sh یک فایل unit برای شما ایجاد میکند.
exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh statusاجرای svc.sh به دسترسی root نیاز دارد، زیرا یک unit را در /etc/systemd/system مینویسد و آن را فعال میکند. آرگومان بعد از install کاربری است که سرویس با حساب او اجرا میشود. gharunner را بهصورت صریح ارسال کنید. اگر آرگومانی مشخص نشود، اسکریپت از $SUDO_USER استفاده میکند که حساب مدیر شماست. در این حالت، همه jobها با کاربری اجرا میشوند که میتواند از sudo استفاده کند.
نام unit از نام repository و runner تشکیل میشود و قالب آن actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service است. لازم نیست این نام را خودتان وارد کنید:
systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pagerیک runner سالم √ Connected to GitHub را در log ثبت میکند و سپس خطی را ثبت میکند که به Listening for Jobs ختم میشود. صفحه Runners مربوط به repository نیز وضعیت آن را Idle نشان میدهد. runnerی که وضعیت Offline دارد، یا در حال اجرا نیست یا نمیتواند از طریق پورت 443 به GitHub دسترسی پیدا کند.
ارسال یک job به runner
runs-on یک runner را بر اساس label انتخاب میکند. self-hosted را بههمراه label خودتان درخواست کنید تا job روی runnerی اجرا نشود که مدنظر شما نبوده است.
name: build
on:
push:
branches: [main]
jobs:
build:
runs-on: [self-hosted, linux, vps]
steps:
- uses: actions/checkout@v5
- run: uname -aاگر job در Waiting for a runner to pick up this job منتظر بماند، labelها با هم مطابقت ندارند. هر label در runs-on باید روی runner وجود داشته باشد؛ بنابراین وجود حتی یک واژه اضافی باعث میشود job بدون نمایش خطا در هیچجا، در صف باقی بماند. این فهرست را با labelهایی که در کنار runner در تنظیمات repository نمایش داده میشوند، مقایسه کنید.
چرا runnerهای self-hosted و repositoryهای عمومی با هم سازگار نیستند
این همان بخشی است که بسیاری از افراد نادیده میگیرند. راهنمای GitHub صریح است: runnerهای self-hosted «تقریباً هرگز نباید برای repositoryهای عمومی استفاده شوند» و «برای اجرا در ماشینهای مجازی موقت و پاک تضمینی ندارند و ممکن است با کد غیرقابلاعتماد موجود در یک workflow، بهطور پایدار به خطر بیفتند».
سازوکار ساده است. یک pull request از یک fork، نسخه کپی خودش از فایل workflow را همراه میآورد. اگر repository عمومی شما workflowهای pull request را روی runner خود اجرا کند، هر کسی که بتواند repository را fork کند میتواند workflowای پیشنهاد دهد که commandهای او را روی VPS شما اجرا کند. این فرد به دسترسی نوشتن نیاز ندارد، چون همان چیزی که پیشنهاد میدهد، اجرا میشود.
تنظیمات تأیید این مشکل را کاهش میدهند، اما آن را برطرف نمیکنند. سیاست پیشفرض برای یک repository عمومی از maintainer میخواهد workflow مربوط به fork یک مشارکتکننده جدید را تأیید کند. پس از اینکه یک بار آن فرد را تأیید کردید، pull requestهای بعدی او بدون درخواست جدید اجرا میشوند. بنابراین این دروازه به خواندن diff توسط یک انسان، در هر نوبت، وابسته است و نادیده گرفتن یک payload که سه سطح پایینتر در یک build script پنهان شده، آسان است.
یک pull request از fork، secrets شما را دریافت نمیکند و GITHUB_TOKEN آن فقط خواندنی است. این موضوع دامنه آسیب داخل GitHub را محدود میکند، اما برای server شما هیچ کاری انجام نمیدهد. مهاجم در نقش gharunner به shell دسترسی دارد؛ بنابراین میتواند هر فایلی را که آن user قادر به خواندنش است بخواند، به هر چیزی که VPS از طریق شبکه خصوصی خود میتواند به آن دسترسی پیدا کند برسد و چیزی را در ~/.bashrc یا یک user systemd unit باقی بگذارد تا در job بعدی اجرا شود.
ثبت runner با --ephemeral باعث میشود runner یک job را بپذیرد و سپس از ثبت خارج شود؛ بنابراین یک job نمیتواند workspace مربوط به job بعدی را بخواند. این کار فقط زمانی مفید است که چیزی برای هر job، machine یا container را دوباره بسازد، زیرا backdoor نوشتهشده در home directory مربوط به runner user پس از ثبت مجدد نیز باقی میماند.
قواعد بعدی کوتاه هستند. برای repositoryهای خصوصی از runnerهای self-hosted استفاده کنید. اگر مجبورید یکی را به یک repository عمومی متصل کنید، pull requestهای fork را روی آن اجرا نکنید، هیچ چیز دیگری روی آن server نگه ندارید و machine را موقتی فرض کنید.
کارهای Docker و گروهی که عملاً root است
کارهای مربوط به کانتینر، کانتینرهای سرویس و هر مرحله از workflow که docker build را فراخوانی میکند، به یک Docker daemon روی میزبان runner نیاز دارند. Docker را به روش معمول نصب کنید؛ راهنمای Docker و Docker Compose روی VPS این کار را پوشش میدهد. سپس کاربر runner را به گروه docker اضافه کنید.
پیش از انجام این کار، پیامدهای آن را درک کنید. عضویت در گروه docker معادل دسترسی root است، زیرا یک کانتینر میتواند / را بهصورت bind mount متصل کند و داخل آن با کاربر root اجرا شود. بنابراین workflowای که میتواند با سوکت Docker ارتباط برقرار کند، قادر است همه فایلهای VPS، از جمله /etc/shadow، را بخواند و تغییر دهد. در یک مخزن خصوصی با مشارکتکنندگان مورداعتماد، این خطر ممکن است قابلقبول باشد. در هر محیط دیگری، این کار هدف استفاده از کاربر بدون امتیاز را از بین میبرد. Rootless Docker ساخت کانتینرها را در محدوده دسترسیهای خود کاربر runner نگه میدارد، اما درایور ذخیرهسازی کندتر میشود و کانتینرهای دارای دسترسی ممتاز قابلاستفاده نخواهند بود.
بهروزرسانی و حذف صحیح runner
یک self-hosted runner بهصورت پیشفرض خودش را بهروزرسانی میکند. با مشاهده یک release جدید، فایلهای خود را جایگزین میکند و سرویس را دوباره راهاندازی میکند؛ بنابراین معمولاً کاری لازم نیست انجام دهید. ./config.sh --disableupdate بهروزرسانی خودکار را غیرفعال میکند تا بتوانید از یک نسخه ثابت استفاده کنید. پس از آن، بهروزرسانی بر عهده شماست: مستندات GitHub صراحتاً اعلام میکنند که runner پیکربندیشده با --disableupdate باید بهصورت دستی بهروزرسانی شود.
بهروزرسانی دستی ثبت runner را حفظ میکند، زیرا .runner و .credentials داخل tarball نیستند. سرویس را متوقف کنید، tarball جدید را با gharunner بارگیری و checksum آن را بررسی کنید، سپس آن را با tar xzf روی همان directory استخراج کنید و سرویس را دوباره راهاندازی کنید:
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh startبرای حذف runner، ابتدا سرویس را uninstall کنید و سپس آن را deregister کنید. توکن حذف از همان صفحه Runners و از دکمه Remove مربوط به خود runner دریافت میشود.
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh uninstall
sudo -iu gharunner
cd ~/actions-runner
./config.sh remove --token PASTE_REMOVAL_TOKEN_HEREحذف directory بدون deregister کردن، runner را در repository با وضعیت Offline باقی میگذارد، زیرا GitHub فقط زمانی از حذف آن مطلع میشود که runner این موضوع را اعلام کند یا administrator ورودی را بهصورت دستی حذف کند.
خطاهای متداول، همراه با رشتههایی که مشاهده خواهید کرد
Must not run with sudo. config.sh هنگام اجرا با حساب root این پیام را چاپ میکند و خارج میشود. این بررسی عمدی است، زیرا فایلهای متعلق به root در _work باعث شکست همه کارهای بعدی میشوند که با کاربر سرویس اجرا میشوند. ./config.sh را بهعنوان gharunner اجرا کنید. متغیر RUNNER_ALLOW_RUNASROOT این بررسی را نادیده میگیرد و استفاده از آن فقط بروز مشکل را به مرحله بعد موکول میکند.
sudo: ./svc.sh: command not found. شما در پوشه درست قرار دارید. svc.sh هنوز وجود ندارد، زیرا config.sh هنوز ثبتنامی را کامل نکرده است. runner را ثبت کنید، سپس سرویس را نصب کنید.
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. این token یک registration token معتبر نیست. یا منقضی شده است، زیرا فقط یک ساعت اعتبار دارد، یا یک personal access token بهجای registration token از صفحه Runners جایگذاری شده است. یک token جدید ایجاد کنید و دوباره آن را جایگذاری کنید.
Dependencies is missing for Dotnet Core 6.0. sudo ./bin/installdependencies.sh را از پوشه runner و با حساب root اجرا کنید، سپس دوباره ثبتنام کنید.
پس از راهاندازی مجدد، Runner Offline است. systemctl is-enabled 'actions.runner.*' را اجرا کنید. اگر چیزی فهرست نشد، ./svc.sh install هرگز اجرا نشده است؛ بنابراین runner فقط درون نشست terminal شما وجود داشته است. اگر unit فعال است و runner همچنان Offline است، journalctl -u 'actions.runner.*' را بخوانید و اتصال HTTPS خروجی را بررسی کنید.
دیسک پر میشود. checkoutها، cacheهای build و imageهای Docker در _work و home کاربر runner انباشته میشوند و چیزی آنها را بهصورت خودکار پاک نمیکند. du -sh /home/gharunner/actions-runner/_work را پایش کنید و پیش از آنکه دیسک خودش تصمیم بگیرد، یک پاکسازی زمانبندیشده اضافه کنید.
FAQ
چرا sudo ./svc.sh install پیام command not found را نشان میدهد؟
زیرا svc.sh در tarball مربوط به runner وجود ندارد. این فایل پس از تکمیل ثبت ./config.sh، در دایرکتوری runner ایجاد میشود و با استفاده از نام repository و runner شما، نام سرویس را میسازد. ابتدا ./config.sh را با کاربر runner اجرا کنید. پس از آن، sudo ./svc.sh install gharunner اسکریپت را پیدا میکند و unitای با نام actions.runner.OWNER-REPO.RUNNER-NAME.service را در /etc/systemd/system مینویسد.
آیا برای self-hosted runner باید پورتی را در فایروال باز کنم؟
خیر. runner یک اتصال HTTPS خروجی به GitHub باز میکند و هنگام انتظار برای jobها آن را باز نگه میدارد؛ بنابراین GitHub هرگز اتصالی را به VPS شما آغاز نمیکند. ترافیک خروجی روی پورت 443 را مجاز کنید و قوانین ورودی را بسته نگه دارید. اگر runner درحالیکه سرویس آن در حال اجراست، وضعیت Offline را نشان میدهد، بهجای قوانین ورودی، فیلتر ترافیک خروجی و DNS را بررسی کنید.
آیا میتوانم از self-hosted runner در یک repository عمومی استفاده کنم؟
بله، اما GitHub این کار را توصیه نمیکند. یک pull request از fork فایل workflow خود را دارد؛ بنابراین هر کسی که بتواند repository شما را fork کند، میتواند commandهایی را پیشنهاد دهد که روی دستگاه شما اجرا شوند. پیام تأیید فقط نخستین اجرا توسط یک مشارکتکننده را پوشش میدهد. اگر runner را به یک repository عمومی متصل میکنید، workflowهای pull request از fork را در آن غیرفعال کنید، هیچ مورد دیگری را روی آن server نگه ندارید و دستگاه را طبق یک برنامه زمانی بازسازی کنید.
چرا ثبت با Http response code: NotFound با شکست مواجه میشود؟
فراخوانی ثبت، هنگامی که credential نادرست است، پاسخ NotFound میدهد؛ نه فقط زمانی که URL نادرست باشد. به همین دلیل این پیام گمراهکننده است. registration tokenها یک ساعت پس از نمایش منقضی میشوند و personal access token برای این فراخوانی پذیرفته نمیشود. دوباره Settings، Actions، Runners، New self-hosted runner را باز کنید، token جدید را کپی کنید و مطمئن شوید مقدار --url به repositoryای اشاره میکند که در آن مجوز administrator دارید.