إعداد مشغّل GitHub Actions على VPS بنظام Ubuntu 24.04
سجّل مشغّل GitHub Actions ذاتي الاستضافة على Ubuntu 24.04: أنشئ مستخدماً مخصصاً، تحقّق من checksum، شغّل config.sh وخدمة systemd، وافهم خطر طلبات السحب.
ما الذي يفعله مشغّل GitHub Actions مستضاف ذاتياً
مشغّل GitHub Actions مستضاف ذاتياً هو برنامج تثبّته على VPS تملكه، ويطلب من GitHub المهام ثم يشغّلها على أجهزتك. تسجّله مع مستودع واحد، وتثبّته كخدمة systemd، فيعمل مجدداً بعد كل إعادة تشغيل. يحدّد GitHub المهمة التي يجب تشغيلها. وينفّذ خادمك العمل.
تستحق CI (التكامل المستمر) على خادم تملكه ذلك لسببين. لا تعود دقائق البناء محسوبة ضمن حد استخدام، ويمكن للمهمة الوصول إلى موارد لا تتوفر إلا على جهازك، مثل ذاكرة تخزين مؤقت للبناء محتفظة بها أو شبكة خاصة. لكن المقابل هو الأمان. ينفّذ المشغّل كل ما يحدده ملف سير العمل، وبصلاحيات المستخدم الذي خصصته له؛ لذلك فإن ملف سير العمل يتيح تنفيذ التعليمات البرمجية عن بُعد بحكم تصميمه. يكون ذلك مقبولاً في مستودع خاص، لأن الأشخاص الذين تثق بهم فقط يمكنهم إضافة ملف سير عمل. أما في مستودع عام، فذلك يمثل خطراً حقيقياً، ويشرح القسم الخاص بطلبات السحب من التفرعات آلية حدوثه.
تستخدم جميع الخطوات أدناه Ubuntu 24.04 وإصدار المشغّل 2.336.0، وهو الإصدار الحالي حتى July 2026.
ما تحتاج إليه قبل البدء
ابدأ من VPS يضم حساب مسؤول عادي مع صلاحيات sudo، وبالحالة التي تصل إليها في الدقائق العشر الأولى على VPS جديد. لا تحتاج إلى فتح منفذ وارد. يفتح المشغّل اتصال HTTPS (بروتوكول نقل النص التشعبي الآمن) صادراً إلى GitHub، ويبقيه مفتوحاً أثناء انتظار المهام، لذلك لا يتصل GitHub بخادمك أبداً. يمكن أن يظل جدارك الناري مغلقاً أمام العالم، ومع ذلك تصل المهام.
تحتاج أيضاً إلى صلاحيات المسؤول على المستودع، لأن رمز التسجيل يظهر في إعدادات المستودع.
أنشئ مستخدماً مخصصاً للـrunner
لا تشغّل الـrunner مطلقاً بصفة root أو باستخدام حسابك الإداري. ترث كل مهمة صلاحيات مستخدم الـrunner، لذلك ينجح سير عمل يستدعي sudo إذا كان مستخدم الـrunner يستطيع استخدام sudo. أنشئ مستخدماً واحداً غير مميّز لا يملك شيئاً باستثناء دليله الرئيسي. يشرح حسابات المستخدمين ذات أقل الصلاحيات على 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-runnerيقفل passwd -l كلمة المرور، لذلك لا يستطيع أحد تسجيل الدخول بصفة gharunner باستخدام كلمة مرور. ويهم ضبط الوضع 700 على دليل الـrunner لأن الـrunner يخزّن بيانات الاعتماد فيه بنص واضح، وقد يحتوي checkout على مصدر خاص.
تحقق من الخاصيتين قبل المتابعة:
sudo passwd -S gharunner
sudo -l -U gharunnerيطبع passwd -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 القيمة الخاصة بالإصدار الحالي في صفحة الإصدار وعلى شاشة 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 يكتشف المشكلة بدلاً منك. يفشل الأرشيف المكتوب جزئياً مع gzip: stdin: unexpected end of file وtar: Unexpected EOF in archive، وهذا يخبرك بأن الملف تالف، لكنه لا يحدد ما إذا كان قد اقتُطع أو استُبدل.
tar xzf ./actions-runner-linux-x64-2.336.0.tar.gz
lsمحتويات tarball وما لا يحتوي عليه
بعد الاستخراج، يحتوي الدليل على config.sh وrun.sh وenv.sh وsafe_sleep.sh وbin/ وexternals/. يحتوي bin/ على الملفات الثنائية الخاصة بالتشغيل وbin/installdependencies.sh. ويحتوي externals/ على بيئة Node المضمّنة التي تُنفَّذ عليها إجراءات JavaScript.
لا يوجد svc.sh بعد. تصف وثائق GitHub هذا الملف بأنه البرنامج النصي «الذي يُنشأ بعد إضافة runner بنجاح»، لأنه يُكتب من قالب يتضمن اسم المستودع واسم runner ضمن اسم الخدمة. لذلك يفشل sudo ./svc.sh install قبل ./config.sh مع sudo: ./svc.sh: command not found. سجّل runner أولاً، ثم ثبّت الخدمة.
ثبّت تبعيات الـrunner
الـrunner تطبيق .NET، لذلك يحتاج إلى بعض المكتبات المشتركة. أبقِ shell الخاص بمستخدم الـrunner كما هو، وثبّت هذه المكتبات باستخدام sudo، لأن السكربت يكتب في قاعدة بيانات حزم النظام.
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.shعلى Ubuntu 24.04، يثبّت ذلك libkrb5-3 وzlib1g وliblttng-ust1t64 وlibssl3t64 وlibicu74. يجرّب السكربت عدة أسماء للإصدارات لكل مكتبة، ويحتفظ بالاسم الذي يتوفر في إصدارك. لذلك يعمل السكربت نفسه على إصدارات 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 على المكتبات المضمّنة قبل أن يبدأ، لذلك يؤدي تعذّر حل الرابط إلى إيقاف السكربت بدلاً من التسبب في تعطل مربك لاحقاً.
تسجيل الـrunner في مستودعك
احصل على رمز من المستودع. افتح Settings، ثم Actions، ثم Runners، ثم New self-hosted runner. تعرض الصفحة رمز تسجيل يبدأ بـ A. تنتهي صلاحيته بعد ساعة واحدة من إنشائه، لذلك أنشئه عندما تكون مستعداً للصقه.
سجّل الـ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وظيفة هذه الخيارات. يحدد --name الاسم الذي يظهر به الـrunner في المستودع، لذلك اختر اسماً ستظل قادراً على التعرّف إليه بعد ستة أشهر. يضيف --labels التسميات التي تختارها؛ ويحمل الـrunner مسبقاً self-hosted وLinux وX64 دون الحاجة إلى طلب ذلك. يحدد --work الدليل الذي توضع فيه عمليات checkout داخل دليل الـrunner. يجيب --unattended عن المطالبات التفاعلية باستخدام قيمها الافتراضية، وهذا هو المطلوب عندما يكون الأمر موجوداً داخل برنامج نصي. يستبدل --replace تسجيلًا موجوداً بالاسم نفسه بدلاً من الفشل، وهذا هو المطلوب عند إعادة بناء الخادم.
تنتهي العملية الناجحة بالأسطر التالية:
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.يوجد التسجيل الآن داخل دليل الـrunner في الملفات .runner و.credentials و.credentials_rsaparams. يعرّف الملفان الأخيران هذا الـrunner لدى GitHub، لذلك يمكن لأي شخص يستطيع قراءتهما انتحال هويته. ولهذا يكون نمط الدليل 700 ولا يملك المستخدم صلاحية sudo.
ثبّت الـrunner كخدمة systemd
يعمل ./run.sh في الطرفية لاختبار واحد، لكنه يتوقف بانتهاء جلسة SSH. ثبّت الخدمة لكي يبدأ الـrunner عند الإقلاع. يشرح خدمات systemd والمؤقتات على VPS ملفات الوحدة نفسها. أما svc.sh فينشئ ملف الوحدة نيابةً عنك.
exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh statusيتطلب svc.sh صلاحيات root لأنه يكتب وحدة في /etc/systemd/system ويمكّنها. الوسيط الذي يلي install هو المستخدم الذي تعمل الخدمة باسمه. مرّر gharunner صراحةً. إذا لم تمرّر وسيطاً، يعود البرنامج النصي إلى $SUDO_USER، وهو حساب المسؤول لديك، وعندها تُشغّل كل مهمة باسم مستخدم يمكنه استخدام sudo.
تُسمّى الوحدة باسم المستودع والـ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 ثم سطراً ينتهي بـListening for Jobs، وتعرض صفحة Runners في المستودع حالته Idle. إذا ظهر الـrunner بالحالة Offline، فهذا يعني أنه لا يعمل أو لا يستطيع الوصول إلى GitHub عبر المنفذ 443.
إرسال مهمة إلى المنفّذ
يحدّد runs-on منفّذاً وفقاً للتسمية. اطلب self-hosted مع التسمية الخاصة بك، حتى لا تُسنَد المهمة إلى منفّذ لم تقصده.
name: build
on:
push:
branches: [main]
jobs:
build:
runs-on: [self-hosted, linux, vps]
steps:
- uses: actions/checkout@v5
- run: uname -aإذا انتظرت المهمة عند Waiting for a runner to pick up this job، فهذا يعني أن التسميات غير متطابقة. يجب أن تكون كل تسمية في runs-on موجودة على المنفّذ، لذلك قد تؤدي كلمة إضافية واحدة إلى بقاء المهمة في قائمة الانتظار من دون ظهور أي خطأ. قارن القائمة بالتسميات الظاهرة بجانب المنفّذ في إعدادات المستودع.
لماذا لا تتوافق أنظمة التشغيل الذاتية الاستضافة مع المستودعات العامة
هذا هو الجزء الذي يتجاوزه كثيرون. إرشادات GitHub صريحة: «يجب تقريباً عدم استخدام أنظمة التشغيل الذاتية الاستضافة مع المستودعات العامة»، كما أنّها «لا تضمن التشغيل داخل أجهزة افتراضية مؤقتة ونظيفة، وقد تتعرض لاختراق مستمر بسبب تعليمات برمجية غير موثوقة في سير العمل».
الآلية بسيطة. يجلب طلب السحب من fork نسخته الخاصة من ملف سير العمل. إذا كان مستودعك العام يشغّل سير العمل الخاص بطلبات السحب على نظام التشغيل لديك، فيمكن لأي شخص يستطيع إنشاء fork للمستودع اقتراح سير عمل يشغّل أوامره على VPS لديك. لا يحتاج إلى صلاحية الكتابة، لأن الشيء الذي يقترحه هو نفسه الشيء الذي سيُشغَّل.
تخفف إعدادات الموافقة من هذا الخطر، لكنها لا تعالجه. تطلب السياسة الافتراضية للمستودع العام من أحد المشرفين الموافقة على سير عمل fork الخاص بأول مساهمة من شخص جديد. بعد أن توافق على ذلك الشخص مرة واحدة، تُشغَّل طلبات السحب اللاحقة التي يرسلها دون طلب جديد. لذلك تعتمد الحماية على أن يقرأ شخص الفرق في كل مرة، ومن السهل أن يفوته حمْلٌ ضار مخفي على ثلاثة مستويات داخل سكربت البناء.
لا يتلقى طلب السحب من fork أسرارك، وتكون صلاحية GITHUB_TOKEN للقراءة فقط. يحد ذلك من الضرر داخل GitHub، لكنه لا يحمي خادمك. يمتلك المهاجم shell بصلاحيات gharunner، ولذلك يستطيع قراءة كل ملف يمكن لذلك المستخدم قراءته، والوصول إلى أي مورد يستطيع VPS الوصول إليه عبر شبكته الخاصة، وترك مكوّن ضار في ~/.bashrc أو في وحدة systemd للمستخدم تُشغَّل أثناء المهمة التالية.
يؤدي التسجيل باستخدام --ephemeral إلى جعل نظام التشغيل يقبل مهمة واحدة ثم يلغي تسجيله، ولذلك لا تستطيع مهمة قراءة مساحة عمل المهمة التالية. يفيد ذلك فقط إذا أعاد شيء ما إنشاء الجهاز أو الحاوية لكل مهمة، لأن باباً خلفياً كُتب في الدليل الرئيسي لمستخدم نظام التشغيل يبقى بعد تسجيل جديد.
القواعد التالية قصيرة. استخدم أنظمة التشغيل الذاتية الاستضافة مع المستودعات الخاصة. إذا اضطررت إلى ربط أحدها بمستودع عام، فلا تشغّل طلبات السحب من fork عليه، ولا تضع أي شيء آخر على ذلك الخادم، وتعامل مع الجهاز على أنه مؤقت ويمكن التخلص منه.
مهام Docker، والمجموعة التي تعادل root فعلياً
تحتاج مهام الحاويات وحاويات الخدمات وأي خطوة في سير العمل تستدعي docker build إلى Docker daemon على مضيف runner. ثبّت Docker بالطريقة المعتادة، كما يوضّح ذلك Docker وDocker Compose على VPS، ثم أضف مستخدم runner إلى مجموعة docker.
افهم المقايضة قبل تنفيذ ذلك. تعادل العضوية في مجموعة docker امتيازات root، لأن الحاوية يمكنها إجراء bind mount للمسار / وتشغيل عمليات بصلاحيات root داخلها. لذلك، يمكن لسير عمل يتصل بـDocker socket قراءة كل ملف على VPS وكتابته، بما في ذلك /etc/shadow. قد يكون ذلك مقبولاً في مستودع خاص يساهم فيه أشخاص موثوقون. أما في أي سياق آخر، فهو يلغي فائدة المستخدم غير ذي الامتيازات. يحافظ Rootless Docker على عمليات إنشاء الحاويات ضمن صلاحيات مستخدم runner نفسه، لكن على حساب استخدام برنامج تشغيل تخزين أبطأ وعدم دعم الحاويات ذات الامتيازات.
التحديث وإزالة الـrunner بطريقة صحيحة
يحدّث الـrunner المستضاف ذاتياً نفسه افتراضياً. يكتشف الإصدار الجديد، ويستبدل ملفاته، ويعيد تشغيل الخدمة، لذلك لا تحتاج عادةً إلى تنفيذ أي إجراء. يعطّل ./config.sh --disableupdate التحديث الذاتي عندما تحتاج إلى تثبيت إصدار محدد. بعد ذلك تصبح مسؤولية التحديث عليك: توضح وثائق GitHub أن الـrunner المُعد باستخدام --disableupdate يجب تحديثه يدوياً.
يحافظ التحديث اليدوي على التسجيل، لأن .runner و.credentials غير موجودين في ملف tarball. أوقف الخدمة، ونزّل ملف tarball الجديد وتحقق من checksum الخاص به باستخدام gharunner، ثم استخرجه فوق الدليل نفسه باستخدام tar xzf، وبعد ذلك شغّل الخدمة مجدداً:
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh startلإزالة الـrunner، أزل الخدمة أولاً، ثم ألغِ تسجيله. يأتي رمز الإزالة من صفحة 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يؤدي حذف الدليل دون إلغاء التسجيل إلى بقاء الـrunner مدرجاً بحالة Offline في المستودع، لأن GitHub لا يعرف أنه أُزيل إلا عندما يبلغه الـrunner بذلك أو يحذف أحد المسؤولين الإدخال يدوياً.
أوضاع الفشل، مع النصوص التي ستظهر لك
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'. الرمز ليس رمز تسجيل صالحاً. انتهت صلاحيته، لأن مدة صلاحيته ساعة واحدة فقط، أو ألصقت personal access token بدلاً من رمز التسجيل الموجود في صفحة Runners. أنشئ رمزاً جديداً وألصقه مرة أخرى.
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 موجوداً إلا داخل جلسة الطرفية. إذا كانت الوحدة مفعّلة وما زال runner في حالة Offline، اقرأ journalctl -u 'actions.runner.*' وتحقق من اتصالات HTTPS الصادرة.
يمتلئ القرص. تتراكم عمليات checkout وذاكرات التخزين المؤقت للبناء وصور Docker ضمن _work وفي الدليل الرئيسي لمستخدم runner، ولا يحذفها شيء تلقائياً. راقب du -sh /home/gharunner/actions-runner/_work وأضف عملية تنظيف مجدولة قبل أن يمتلئ القرص.
FAQ
لماذا يعرض sudo ./svc.sh install الرسالة command not found؟
لأن svc.sh غير موجود في حزمة tarball الخاصة بـ runner. يُنشأ هذا الملف في دليل runner عندما ينتهي ./config.sh من التسجيل، باستخدام اسم المستودع واسم runner لإنشاء اسم الخدمة. شغّل ./config.sh أولاً بصفته مستخدم runner. بعد ذلك، يعثر sudo ./svc.sh install gharunner على البرنامج النصي ويكتب وحدة باسم actions.runner.OWNER-REPO.RUNNER-NAME.service داخل /etc/systemd/system.
هل أحتاج إلى فتح منفذ في الجدار الناري لـ runner مستضاف ذاتياً؟
لا. يفتح runner اتصال HTTPS صادراً إلى GitHub ويبقيه مفتوحاً أثناء انتظار المهام، لذلك لا يبدأ GitHub أي اتصال بخادم VPS لديك. اسمح بالاتصالات الصادرة عبر 443، وأبقِ قواعد الاتصالات الواردة مغلقة. إذا ظهر runner بالحالة Offline أثناء تشغيل خدمته، فتحقق من تصفية الاتصالات الصادرة وDNS بدلاً من فحص القواعد الواردة.
هل يمكنني استخدام runner مستضاف ذاتياً مع مستودع عام؟
نعم، لكن GitHub لا ينصح بذلك. يحتوي طلب السحب من fork على ملف workflow الخاص به، لذلك يمكن لأي شخص يستطيع إنشاء fork لمستودعك اقتراح أوامر تُنفَّذ على جهازك. تغطي مطالبة الموافقة التشغيل الأول للمساهم فقط. إذا ربطت runner بمستودع عام، فعطّل workflows الخاصة بطلبات السحب من fork عليه، ولا تشغّل أي شيء آخر على ذلك الخادم، وأعد بناء الجهاز وفق جدول دوري.
لماذا يفشل التسجيل مع Http response code: NotFound؟
تعرض مكالمة التسجيل الرد NotFound عندما تكون بيانات الاعتماد خاطئة، وليس فقط عندما يكون عنوان URL خاطئاً، لذلك تكون الرسالة مضللة. تنتهي صلاحية رموز التسجيل بعد ساعة واحدة من عرضها، ولا يُقبل personal access token لهذه المكالمة. افتح Settings ثم Actions ثم Runners ثم New self-hosted runner مرة أخرى، وانسخ الرمز الجديد، وتأكد من أن قيمة --url تشير إلى مستودع تملك فيه صلاحيات المسؤول.