SSD Nodes Learn 8GB RAM — $66/سنة
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-01

تثبيت مشغّل GitHub Actions ذاتي الاستضافة على VPS

سجّل مشغّل GitHub Actions على Ubuntu 24.04 بأمان: مستخدم مخصص، فحص checksum، إعداد config.sh، خدمة systemd، ومخاطر طلبات السحب من التفرعات.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

ما الذي يفعله مشغّل GitHub Actions المستضاف ذاتيًا

مشغّل GitHub Actions المستضاف ذاتيًا هو برنامج تثبّته على VPS الخاص بك. يطلب المشغّل المهام من GitHub وينفّذها على أجهزتك. تسجّله في مستودع واحد، وتثبّته كخدمة systemd، ثم يعاود التشغيل بعد كل إعادة تشغيل. يحدد GitHub موعد تنفيذ المهمة. وينفّذ خادمك العمل.

يستحق CI (التكامل المستمر) على خادم تملكه لسببين. لن تعود دقائق الإنشاء محسوبة ضمن حدّ استخدام. كما يمكن للمهمة الوصول إلى موارد لا يملكها إلا جهازك، مثل ذاكرة تخزين مؤقت للإنشاء جاهزة أو شبكة خاصة. لكن المقابل هو الأمان. ينفّذ المشغّل كل ما يحدده ملف سير العمل، وبصلاحيات المستخدم الذي منحته إياها. لذلك فإن ملف سير العمل هو تنفيذ تعليمات عن بُعد بحكم تصميمه. يكون ذلك مقبولًا في مستودع خاص، لأن الأشخاص الذين تثق بهم فقط يمكنهم إضافة ملف. أما في مستودع عام، فهذه مخاطرة حقيقية. يشرح قسم طلبات السحب من التفرعات آلية حدوثها.

تستخدم جميع الأمثلة أدناه Ubuntu 24.04 مع إصدار المشغّل 2.336.0، وهو الإصدار الحالي في يوليو 2026.

ما تحتاج إليه قبل البدء

ابدأ من VPS يحتوي على حساب مسؤول عادي مع sudo، وهي الحالة التي تصل إليها في الدقائق العشر الأولى على VPS جديد. لا تحتاج إلى فتح منفذ وارد. يفتح العِقد اتصال HTTPS (بروتوكول نقل النص التشعبي الآمن) صادرًا إلى GitHub ويبقيه مفتوحًا أثناء انتظار المهام، لذلك لا يتصل GitHub بخادمك مطلقًا. يمكن أن يبقى جدار الحماية مغلقًا أمام العالم، وستظل المهام تصل.

تحتاج أيضًا إلى صلاحيات المسؤول في المستودع، لأن رمز التسجيل يظهر في إعدادات المستودع.

إنشاء مستخدم مخصص للعدّاء

لا تشغّل العدّاء بصفة root أو باستخدام حسابك الإداري. ترث كل مهمة صلاحيات مستخدم العدّاء، لذلك ينجح سير العمل الذي يستدعي sudo إذا كان مستخدم العدّاء يستطيع استخدام 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 على دليل العدّاء لأن العدّاء يخزن بيانات الاعتماد فيه بنص واضح، وقد يحتوي 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، وأن العزل الذي أنشأته للتو لم يعد قائمًا.

تنزيل العَوامِل والتحقق من أرشيف 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 بأنه البرنامج النصي «الذي يتم إنشاؤه بعد إضافة المشغّل بنجاح»، لأنه يُكتب من قالب يتضمن اسم المستودع واسم المشغّل ضمن اسم الخدمة. لذلك يفشل sudo ./svc.sh install قبل ./config.sh مع sudo: ./svc.sh: command not found. سجّل المشغّل أولًا، ثم ثبّت الخدمة.

تثبيت تبعيات المشغّل

المشغّل تطبيق .NET، لذلك يحتاج إلى بعض المكتبات المشتركة. أبقِ صدفة مستخدم المشغّل كما هي، وثبّت المكتبات باستخدام 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 على المكتبات المضمّنة قبل أن يبدأ، لذلك يؤدي الرابط غير المحلّ إلى إيقاف البرنامج النصي بدلًا من التسبب في تعطل مربك لاحقًا.

تسجيل العامل مع مستودعك

احصل على رمز من المستودع. افتح Settings، ثم Actions، ثم Runners، ثم New self-hosted runner. تعرض الصفحة رمز تسجيل يبدأ بـ A. تنتهي صلاحيته بعد ساعة واحدة من إنشائه، لذلك أنشئه عندما تكون مستعدًا للصقه.

سجّل العامل باستخدام حساب العامل. يرفض 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 الاسم الذي يظهر به العامل في المستودع، لذلك اختر اسمًا ستظل قادرًا على تمييزه بعد ستة أشهر. يضيف --labels التسميات الخاصة بك؛ ويحمل العامل مسبقًا self-hosted وLinux وX64 من دون طلب ذلك. يحدد --work اسم الدليل الذي توضع فيه عمليات checkout، داخل دليل العامل. يجيب --unattended عن المطالبات التفاعلية باستخدام قيمها الافتراضية، وهذا هو المطلوب عندما يكون الأمر داخل نص برمجي. يتولى --replace تسجيلًا موجودًا بالاسم نفسه بدلًا من الفشل، وهذا هو المطلوب عند إعادة إنشاء الخادم.

ينتهي التشغيل الناجح بهذه الأسطر:

√ Runner successfully added
√ Runner connection is good
√ Settings Saved.

يوجد التسجيل الآن في دليل العامل على شكل .runner و.credentials و.credentials_rsaparams. يعرّف العنصران الأخيران هذا العامل لدى GitHub، لذلك يمكن لأي شخص يستطيع قراءتهما انتحال هويته. ولهذا السبب يكون وضع الدليل 700 ولا يملك المستخدم صلاحية sudo.

تثبيت المُشغِّل كخدمة systemd

يعمل ./run.sh في الطرفية لاختبار واحد، لكنه يتوقف عند انتهاء جلسة SSH. ثبّت الخدمة لكي يبدأ المُشغِّل عند إقلاع النظام. يشرح خدمات ومؤقتات 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.

تُسمّى الوحدة باسم المستودع والمُشغِّل، بالتنسيق actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. لا تحتاج إلى كتابة ذلك يدويًا:

systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pager

يسجّل المُشغِّل السليم √ Connected to GitHub ثم سطرًا ينتهي بـ Listening for Jobs، وتعرض صفحة Runners في المستودع حالته Idle. يعني ظهور المُشغِّل بالحالة 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 واضحة: ينبغي «عدم استخدام مشغلات المهام المستضافة ذاتيًا تقريبًا مطلقًا مع المستودعات العامة»، كما أنها «لا تضمن التشغيل في أجهزة افتراضية نظيفة ومؤقتة، ويمكن اختراقها بشكل مستمر بواسطة تعليمات برمجية غير موثوقة في سير العمل».

الآلية بسيطة. يجلب طلب السحب من نسخة متفرعة نسخته الخاصة من ملف سير العمل. إذا كان مستودعك العام يشغّل مهام سير عمل طلبات السحب على مشغلك، فيمكن لأي شخص يستطيع إنشاء نسخة متفرعة من المستودع اقتراح سير عمل يشغّل أوامره على VPS الخاص بك. لا يحتاج إلى صلاحية الكتابة، لأن الشيء الذي يقترحه هو نفسه الشيء الذي سيُشغَّل.

تخفف إعدادات الموافقة من هذا الخطر، لكنها لا تعالجه. تطلب السياسة الافتراضية للمستودع العام من أحد المشرفين الموافقة على سير عمل النسخة المتفرعة لمساهم يشارك للمرة الأولى. بعد أن توافق على ذلك الشخص مرة واحدة، تُشغَّل طلبات السحب اللاحقة التي يرسلها من دون طلب جديد. لذلك، تعتمد الحماية على أن يقرأ شخص الفرق في كل مرة، ومن السهل عدم ملاحظة حمولة مخفية في المستوى الثالث داخل برنامج نصي للبناء.

لا يحصل طلب السحب من نسخة متفرعة على أسرارك، وتكون GITHUB_TOKEN الخاصة به للقراءة فقط. يحد ذلك من الضرر داخل GitHub. لكنه لا يحمي خادمك. يحصل المهاجم على shell باسم gharunner، ولذلك يمكنه قراءة كل ملف يستطيع ذلك المستخدم قراءته، والوصول إلى أي شيء يستطيع VPS الوصول إليه عبر شبكته الخاصة، وترك شيء ضار في ~/.bashrc أو في وحدة systemd للمستخدم تعمل أثناء المهمة التالية.

يؤدي التسجيل باستخدام --ephemeral إلى جعل المشغل يقبل مهمة واحدة ثم يلغي تسجيله، لذلك لا تستطيع مهمة قراءة مساحة عمل المهمة التالية. يفيد ذلك فقط إذا أعاد شيء ما إنشاء الجهاز أو الحاوية لكل مهمة، لأن الباب الخلفي المكتوب في الدليل الرئيسي لمستخدم المشغل يبقى بعد التسجيل من جديد.

القواعد التالية قصيرة. استخدم مشغلات المهام المستضافة ذاتيًا مع المستودعات الخاصة. إذا اضطررت إلى إرفاق أحدها بمستودع عام، فلا تشغّل طلبات السحب من النسخ المتفرعة عليه، ولا تضع أي شيء آخر على ذلك الخادم، وتعامل مع الجهاز على أنه قابل للتخلص منه.

مهام 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 نفسه، لكن ذلك يأتي على حساب استخدام برنامج تشغيل تخزين أبطأ وعدم دعم الحاويات ذات الصلاحيات المميزة.

التحديث وإزالة العَداء بشكل سليم

يحدّث العَداء المستضاف ذاتيًا نفسه افتراضيًا. يكتشف الإصدار الجديد، ويستبدل ملفاته، ويعيد تشغيل الخدمة، لذلك لا تحتاج عادةً إلى فعل أي شيء. يعطّل ./config.sh --disableupdate التحديث الذاتي عندما تحتاج إلى إصدار ثابت. بعد ذلك تصبح مسؤولية التحديث عليك: توضح وثائق GitHub أن العَداء المُكوَّن باستخدام --disableupdate يجب تحديثه يدويًا.

يحافظ التحديث اليدوي على التسجيل، لأن .runner و.credentials غير موجودين في ملف tarball. أوقف الخدمة، ونزّل ملف tarball الجديد وتحقّق من مجموع الاختبار الخاص به باعتباره gharunner، ثم استخرجه فوق الدليل نفسه باستخدام tar xzf، وبعد ذلك شغّل الخدمة مجددًا:

cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh start

لإزالة العَداء، ألغِ تثبيت الخدمة أولًا، ثم ألغِ التسجيل. يأتي رمز الإزالة من صفحة Runners نفسها، ضمن زر Remove الخاص بالعَداء.

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

يؤدي حذف الدليل من دون إلغاء التسجيل إلى بقاء العَداء مدرجًا بالحالة Offline في المستودع، لأن GitHub لا يعرف أنه أزيل إلا عندما يبلغه العَداء بذلك أو يحذف المسؤول الإدخال يدويًا.

حالات الفشل، مع السلاسل التي ستظهر لك

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 غير متصل بعد إعادة التشغيل. شغّل systemctl is-enabled 'actions.runner.*'. إذا لم تُعرض أي نتائج، فهذا يعني أن ./svc.sh install لم يُشغّل من قبل، ولذلك لم يوجد runner إلا ضمن جلسة الطرفية. إذا كانت الوحدة مفعّلة وما زال runner غير متصل، اقرأ 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 غير موجود في أرشيف 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 ملف سير العمل الخاص به، لذلك يمكن لأي شخص يستطيع إنشاء fork لمستودعك اقتراح أوامر تُنفَّذ على جهازك. تغطي مطالبة الموافقة التشغيل الأول للمساهم فقط. إذا أضفت runner إلى مستودع عام، فعطّل عليه عمليات سير عمل طلبات السحب من fork، ولا تُبقِ أي شيء آخر على ذلك الخادم، وأعد إنشاء الجهاز وفق جدول زمني.

لماذا يفشل التسجيل مع Http response code: NotFound؟

تُرجع استدعاء التسجيل الإجابة NotFound عندما تكون بيانات الاعتماد غير صحيحة، وليس فقط عندما يكون عنوان URL غير صحيح، وهذا يجعل الرسالة مضللة. تنتهي صلاحية رموز التسجيل بعد ساعة واحدة من عرضها، ولا يُقبل personal access token لهذا الاستدعاء. افتح Settings، ثم Actions، ثم Runners، ثم New self-hosted runner مرة أخرى، وانسخ الرمز الجديد، وتأكد من أن قيمة --url تشير إلى مستودع تملك فيه صلاحيات المسؤول.

#github-actions#ci#self-hosted#runner#ubuntu-24-04