لديك الآن سيرفر Gitea يحفظ مستودعات Repositories الكود الخاصة بك، وبعد كل push تفتح الطرفية Terminal على جهازك وتنفذ docker build ثم docker push بنفسك، وهذه الطريقة تعمل في الأيام الأولى ولكنها تفشل بمرور الوقت، والسبب أن البناء Build يعتمد على جهاز شخص واحد وما عليه من إصدارات، وأن الباسورد الخاص بالـ Registry محفوظ على ذلك الجهاز، وأنك سوف تنسى يوماً ما أن ترفع الـ Image بعد تعديل صغير، فيعمل سيرفر الإنتاج Production بنسخة قديمة دون أن ينتبه أحد.
والحل المعتاد بعد ذلك هو خدمة CI/CD خارجية مثل GitHub Actions أو GitLab CI، وهي تحل مشكلة التكرار، ولكن الكود والأسرار Secrets تخرج من سيرفرك إلى سيرفرات تلك الخدمة مع كل تشغيل Run، وهذا لا يناسب من يستضيف خدماته بنفسه لأنه اختار ذلك أصلاً حتى تبقى بياناته عنده. لذلك فالحل الأنسب أن يتولى سيرفرك هذا العمل بنفسه عبر Gitea Actions، وهو نظام CI/CD مدمج في Gitea وملفاته مكتوبة بصيغة GitHub Actions نفسها تقريباً، حيث تضعها في المجلد .gitea/workflows/ وتستخدم فيها المفاتيح Keys on: وjobs: وsteps:، ومعظم ال actions المنشورة على GitHub تعمل فيه دون تعديل.
ولكن لاحظ أن Gitea لا ينفذ المهام Jobs بنفسه، وإنما يوزعها على برنامج منفصل اسمه Gitea Runner، وهو الذي يسحب المهام وينفذ كل مهمة في Container مستقل، وكان اسمه act_runner حتى الإصدار 0.6، ثم تغير اسمه في مايو 2026 مع الإصدار 1.0 فأصبحت الـ Image الخاصة به باسم gitea/runner.
وسوف نناقش في هذا المقال ما يلي:
- تفعيل Actions في Gitea، والحصول على رمز التسجيل Registration Token للـ runner.
- اختيار نوع الـ Image الخاصة بالـ runner، ولماذا نشغله بطريقة Docker-in-Docker بدلاً من تمرير Docker السيرفر إليه، وما الذي لا يحميك منه هذا العزل.
- كتابة أول workflow يبني Docker Image ويرفعها إلى الـ Container Registry المدمج في Gitea Packages، مستخدماً الأسرار والمتغيرات Variables.
- خطوة نشر اختيارية، ثم التحقق من النجاح والنسخ الاحتياطي والتحديث وأشهر المشكلات وحلولها.
ما تحتاجه قبل أن تبدأ
- Gitea يعمل على
https://git.example.comوقيمةROOT_URLمضبوطة كما في دليل التثبيت، والسبب أن الـ runner يتصل بهذا العنوان، ومنه يستنسخ Clone المستودعات ويرفع الـ Images. - سيرفر للـ runner، والسيرفر نفسه يكفي لفريق صغير، ولكن عندما تكثر عمليات البناء ننصح بسيرفر منفصل لأنها تستهلك المعالج CPU والقرص Disk، وكبداية خصص له 2 vCPU و4 GB من الذاكرة RAM و30 GB على الأقل لتخزين الـ Images مؤقتاً.
- Docker Engine على سيرفر الـ runner، وإذا لم تثبته بعد فابدأ بدليل تثبيت Docker على Ubuntu.
تفعيل Actions في Gitea
ميزة Actions مفعلة افتراضياً منذ Gitea 1.21، وبالتالي لن تحتاج غالباً إلى أي تعديل، ولكن إذا كانت معطلة في نسختك، أي أنك لا ترى تبويب Actions في المستودع ولا في Site Administration، فأضف هذين السطرين إلى خدمة gitea في ملف Compose ثم نفذ docker compose up -d، والمفتاحان مشروحان في مرجع الإعدادات Configuration Cheat Sheet:
GITEA__actions__ENABLED: "true"
# من أين تجلب actions المكتوبة بدون عنوان كامل (مثل actions/checkout@v4)
GITEA__actions__DEFAULT_ACTIONS_URL: githubوالمفتاح الثاني يحدد المكان الذي يجلب منه Gitea ال actions التي تكتبها دون عنوان كامل، فالقيمة github تعني موقع GitHub، أما self فتعني سيرفر Gitea نفسه. بعد ذلك فعل Actions لكل مستودع من Settings ← Repository ← Advanced Settings ← Actions إن لم تكن مفعلة، وتأكد أن سجل الحزم Packages مفعل أيضاً، وهو مفعل افتراضياً.
الحصول على رمز التسجيل Registration Token
يسجل الـ runner نفسه مرة واحدة برمز يصدره Gitea، وللرمز عدة مستويات: المستوى Global لكل المستودعات ويصدر من Site Administration، ومستوى المنظمة Organization أو المستخدم User، ومستوى المستودع الواحد ويصدر من إعداداته. وسوف نبدأ بالمستوى Global، لذلك افتح Site Administration ← Actions ← Runners ← Create new Runner وانسخ Registration Token.

والرمز الواحد يسجل أكثر من runner ويبقى صالحاً حتى تعيد تعيينه، فإذا تسرب فاضغط Reset registration token، ولن تتأثر الـ runners المسجلة من قبل لأن كل واحد منها يحفظ بيانات الاعتماد Credentials الخاصة به بعد التسجيل.
تشغيل Gitea Runner في Container
اختيار نوع الـ Image
للـ Image الخاصة بالـ runner ثلاثة أنواع من الوسوم Tags، والفرق بينها مشروح في دليل التشغيل بـ Docker، ويتلخص في المكان الذي تعمل فيه Containers المهام:
| الوسم | أين تعمل Containers المهام | متى تختاره |
|---|---|---|
gitea/runner:4.1.0 | على Docker السيرفر نفسه عبر /var/run/docker.sock | الأبسط والأسرع، ولكن أي workflow يرى كل Containers السيرفر ويتحكم فيها، فلا تختره إلا إذا كنت تثق تماماً بكل من يرسل كوداً إلى المستودعات. |
gitea/runner:4.1.0-dind | داخل Docker خاص بالـ runner، أي Docker-in-Docker | عزل Isolation أفضل، فالمهام لا ترى Containers السيرفر، ويتطلب الوضع privileged، وهو ما نستخدمه في هذا الدليل. |
gitea/runner:4.1.0-dind-rootless | Docker خاص يعمل دون صلاحيات root | الأكثر أماناً، مع القيود المعروفة لوضع rootless على الشبكة Networking والتخزين Storage. |
وقد يتساءل البعض: لماذا لا نختار النوع الأول وهو الأبسط؟ والإجابة أن تمرير /var/run/docker.sock يعني أن أي workflow يستطيع أن يرى كل Containers السيرفر ويوقفها ويقرأ متغيراتها، بل ويشغل Container جديداً يصل إلى ملفات السيرفر بصلاحيات root، وبالتالي فكل من يملك صلاحية push في أي مستودع يملك السيرفر بالكامل، أما النوع dind فيشغل Docker مستقلاً داخل Container الـ runner، فلا ترى المهام إلا ما بداخله.
ولكن تذكر أن هذا العزل يحميك من workflow يعبث بخدماتك بالخطأ، وليس حاجزاً أمنياً أمام كود خبيث، والسبب أن Container الـ runner يعمل بالوضع privileged الذي يعطيه تقريباً كل صلاحيات السيرفر، وأي مهمة تستطيع أن تطلب من Docker الداخلي تشغيل Container بنفس الوضع ثم تصل منه إلى أجهزة السيرفر. لذلك لا تشغل على هذا الـ runner إلا كوداً تثق به، ولاحظ أن Gitea يطلب موافقة Approval قبل تشغيل ال workflows في طلبات الدمج Pull Requests القادمة من Fork لشخص لا يملك صلاحية الكتابة، فلا توافق عليها قبل أن تقرأ ما عدله في المجلد .gitea/workflows/، وإذا كان المستودع عاماً ويستقبل مساهمات من أشخاص لا تعرفهم فخصص له runner على سيرفر منفصل لا يحمل أي خدمة أخرى.
الملفات
نبدأ بإنشاء مجلد للـ runner على السيرفر:
sudo mkdir -p /opt/gitea-runner
cd /opt/gitea-runnerبعد ذلك ننشئ ملف .env وفيه رمز التسجيل وحده:
GITEA_RUNNER_REGISTRATION_TOKEN=الصق-الرمز-هناثم ملف config.yaml، وأهم ما فيه الـ labels التي تربط ما يكتبه الـ workflow في runs-on بالـ Image التي تنفذ المهمة، وسوف نعرف هنا اثنين: الأول ubuntu-latest ويستخدم الـ Image الجاهزة من Gitea، وفيها Node وgit وأدوات كثيرة ولكنها كبيرة الحجم، والثاني docker ويستخدم الـ Image docker:cli الصغيرة، وهي تكفي لبناء الـ Images ورفعها:
log:
level: info
runner:
capacity: 1
timeout: 1h
labels:
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
- "docker:docker://docker:29.8.1-cli"
container:
network: ""
privileged: falseفي الإعداد أعلاه لاحظ التالي:
capacity: 1يعني أن الـ runner ينفذ مهمة واحدة في نفس الوقت، فارفع القيمة إذا كان السيرفر يتحمل أكثر من عملية بناء.- ترك
networkفارغاً يجعل الـ runner ينشئ شبكة Network خاصة بكل مهمة، ولا تضع فيهbridge، والسبب أن المهام على تلك الشبكة لم تعد تستطيع منذ الإصدار 4.0 الوصول إلى ال cache ولا رفع ال artifacts. privileged: falseخاص بـ Containers المهام وليس بالـ runner نفسه، فالمهام لا تحتاج هذا الوضع لأنها تستخدم Docker الخاص بالـ runner.
وأخيراً ملف docker-compose.yml:
services:
runner:
image: gitea/runner:4.1.0-dind
restart: unless-stopped
privileged: true
env_file: .env
environment:
GITEA_INSTANCE_URL: https://git.example.com/
GITEA_RUNNER_NAME: runner-01
CONFIG_FILE: /config/config.yaml
volumes:
- ./config.yaml:/config/config.yaml:ro
# ملف التسجيل .runner
- runner-data:/data
# Docker Images الخاصة بالمهام، حتى لا تسحب من جديد بعد كل إعادة تشغيل
- runner-docker:/var/lib/docker
volumes:
runner-data:
runner-docker:في الإعداد أعلاه لاحظ التالي:
privileged: trueشرط لعمل Docker الداخلي في النوع dind، وهو سبب التحذير الذي ذكرناه في القسم السابق.- قيمة
GITEA_INSTANCE_URLهي نفس قيمة ROOT_URL في سيرفر Gitea. - ال Volume
runner-dataيحفظ ملف التسجيل.runner، وال Volumerunner-dockerيحفظ ال Images التي تسحبها المهام حتى لا تسحبها من جديد بعد كل إعادة تشغيل.
الآن سوف نقوم بتشغيل الـ runner ومتابعة السجل Log الخاص به:
docker compose up -d
docker compose logs -f runnerفي التشغيل الأول يسجل الـ runner نفسه ثم يبدأ العمل، والمخرج سوف يكون كما يلي:
level=info msg="Runner registered successfully."
level=info msg="runner: runner-01, with version: v4.1.0, with labels: [ubuntu-latest docker], declare successfully"وسوف تجد الـ runner في Site Administration ← Actions ← Runners بحالة Idle:

/data/.runner داخل ال Volume، ولا يعود إلى رمز التسجيل مرة أخرى، لذلك تستطيع حذف الرمز من .env، ولكن إذا حذفت ال Volume فسوف تحتاج إلى رمز جديد، وسيظهر الـ runner في القائمة كأنه جهاز جديد.الأسرار والمتغيرات
حتى يرفع الـ workflow الـ Image يحتاج إلى تسجيل الدخول Login في الـ Registry الخاص بـ Gitea، ولا تستخدم الباسورد الخاص بحسابك لهذا الغرض، والسبب أن من يحصل عليه يستطيع فعل كل شيء باسمك، وإنما أنشئ رمز وصول Access Token مخصصاً من Settings ← Applications ← Generate New Token بصلاحية Scope write:package وحدها، فهي تكفي لتسجيل الدخول ورفع الـ Images، وإذا تسرب الرمز فلن يستطيع من يحمله قراءة الكود أو تعديله. بعد ذلك أضف الرمز إلى المستودع من Settings ← Actions ← Secrets ← Add secret:
REGISTRY_TOKEN: الرمز الذي أنشأته.
ولاحظ أن أسماء الأسرار لا تقبل إلا الحروف الإنجليزية والأرقام والعلامة _، ولا تبدأ برقم ولا بالبادئة GITHUB_ أو GITEA_، فاسم مثل GITEA_TOKEN مرفوض لأنه محجوز للرمز الذي يصدره Gitea تلقائياً لكل مهمة.

أما اسم المستخدم فليس سراً، لذلك ضعه في Variables باسم REGISTRY_USER:

ولماذا نفصل بينهما؟ والسبب أن Gitea يخفي قيمة كل سر في السجلات ويضع مكانها ***، فلو وضعت اسم المستخدم في Secret لاختفى اسم الحساب من كل موضع في السجل، حتى من مسار الـ Image، لذلك اجعل الأسرار للقيم السرية وحدها. وتستطيع أيضاً تعريف الأسرار والمتغيرات على مستوى المنظمة، فتصل إليها كل مستودعاتها.
أول workflow يبني Image ويرفعها
ضع في المجلد الرئيسي للمستودع ملف Dockerfile بسيطاً للتجربة، ومعه ملف index.html بأي محتوى:
FROM nginx:1.29-alpine
COPY index.html /usr/share/nginx/html/index.htmlثم أنشئ الملف .gitea/workflows/ci.yml كما يلي:
name: build-and-push
on:
push:
branches: [main]
workflow_dispatch:
env:
REGISTRY: git.example.com
IMAGE: git.example.com/${{ github.repository }}
jobs:
image:
runs-on: docker
defaults:
run:
shell: sh
steps:
- name: Checkout
uses: builtin:checkout
- name: Log in to the Gitea registry
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u "${{ vars.REGISTRY_USER }}" --password-stdin
- name: Build
run: |
TAG=$(echo "$GITHUB_SHA" | cut -c1-8)
docker build -t "$IMAGE:$TAG" -t "$IMAGE:latest" .
echo "TAG=$TAG" >> "$GITHUB_ENV"
- name: Push
run: |
docker push "$IMAGE:$TAG"
docker push "$IMAGE:latest"
- name: Log out
if: always()
run: docker logout "$REGISTRY"في الإعداد أعلاه لاحظ التالي:
runs-on: dockerيختار الـ label الذي عرفناه، فتعمل المهمة فيdocker:29.8.1-cli، وهذه الـ Image مبنية على Alpine وليس فيها bash، لذلك حددناshell: sh.builtin:checkoutميزة جديدة في Runner 4، حيث تنسخ المستودع دون تنزيل أي action ودون أن تحتاج Image المهمة إلى Node، وهي تقبل جزءاً من خياراتactions/checkoutفقط، أما مع الـ Imageubuntu-latestفتستطيع استخدامactions/checkout@v4المعتاد.- يمرر الـ runner خدمة Docker الخاصة به إلى Container المهمة تلقائياً، فيعمل
docker buildمباشرة، والبناء يجري داخل Docker المعزول الخاص بالـ runner وليس على السيرفر. - عنوان الـ Image في الـ Registry الخاص بـ Gitea يكون دائماً بالصيغة
git.example.com/<owner>/<image>:<tag>، وgithub.repositoryيعطيك القيمةowner/repo، ولكن Docker لا يقبل الحروف الكبيرة في أسماء الـ Images، فإذا كان اسم المستخدم أو المستودع يحتوي على حرف كبير فسوف يفشل البناء بخطأ يقولrepository name must be lowercase، والحل أن تسمي المستودع بحروف صغيرة أو تكتب اسم الـ Image صراحة فيIMAGE.
وقبل أن تضيف أي action من GitHub إلى الـ workflow تذكر أنها كود يعمل على الـ runner الخاص بك ويرى كل الأسرار التي تمررها للمهمة، لذلك ثبتها على الهاش الكامل للـ commit بدلاً من وسم مثل @v4، والسبب أن صاحب المستودع، أو من يخترق حسابه، يستطيع نقل الوسم إلى كود آخر في أي وقت، أما الهاش فلا يتغير.
الآن أضف الملفات إلى المستودع ونفذ push:
git add Dockerfile index.html .gitea/workflows/ci.yml
git commit -m "ci: build and push image"
git pushافتح تبويب Actions في المستودع، وسوف تجد أن التشغيل الأول أبطأ لأن معظم وقته يذهب في سحب الـ Image docker:cli والـ Image الخاصة بـ nginx، أما التشغيلات التالية فأسرع لأن هذه ال Images محفوظة في ال Volume runner-docker:

وبعد انتهاء التشغيل سوف تجد الـ Image في Packages الخاصة بالمالك Owner بوسمين: latest، وأول ثمانية أحرف من هاش الـ commit:

بعد ذلك اربط الحزمة بالمستودع من Settings في صفحة الحزمة حتى تظهر في تبويب Packages الخاص به، واجعلها خاصة Private إذا كان الكود خاصاً، ولسحبها على سيرفر الإنتاج نفذ:
docker login git.example.com
docker pull git.example.com/gitadmin/demo-app:latestخطوة النشر Deployment الاختيارية
بعد رفع الـ Image تستطيع أن تطلب من سيرفر الإنتاج سحبها وتشغيلها، وأبسط طريقة لذلك webhook تستدعيه الخطوة الأخيرة في الـ workflow، مثل webhook لـ stack في Portainer أو خدمة صغيرة على السيرفر:
- name: Deploy
run: wget -q -O- --post-data="" "${{ secrets.DEPLOY_WEBHOOK_URL }}"واحفظ رابط ال webhook في الأسرار وليس في الملف نفسه، والسبب أن من يملك هذا الرابط يستطيع إعادة نشر الخدمة دون أي صلاحية أخرى.
التحقق من النجاح
- الـ runner بحالة Idle في قائمة الـ runners، وآخر ظهور له «now».
- كل push إلى
mainينشئ تشغيلاً جديداً في تبويب Actions، وينتهي بعلامة خضراء. - الحزمة تظهر بوسم جديد، و
docker pullيعمل من جهاز آخر بعدdocker login. - قيمة الرمز لا تظهر في سجل خطوة Login، وإنما يظهر مكانها
***.
النسخ الاحتياطي Backup والاستعادة Restore
ملفات ال workflows وحالة التشغيلات والأسرار والمتغيرات والحزم كلها يحفظها Gitea في قاعدة البيانات Database وفي المجلد /data/gitea/packages، والأمر gitea dump المشروح في دليل التثبيت يغطيها جميعاً، ولكن انتبه إلى حجم الحزم لأن ال Docker Images تكبر بسرعة، لذلك راقب المساحة وأضف قواعد تنظيف Cleanup Rules من Settings ← Packages ← Cleanup rules، مثل الاحتفاظ بآخر 10 وسوم.
أما الـ runner نفسه فلا يحتاج إلى نسخ احتياطي، ويكفي أن تحفظ config.yaml وdocker-compose.yml، وإذا فقدت ال Volume فسجل الـ runner من جديد برمز جديد واحذف القديم من القائمة.
التحديث Upgrade إلى إصدار أحدث
غير وسم الـ Image في docker-compose.yml، ثم نفذ:
docker compose pull
docker compose up -dوالتسجيل محفوظ في Volume البيانات، فلا يحتاج الـ runner إلى رمز جديد، ولكن قبل الانتقال بين الإصدارات الرئيسية Major Versions اقرأ ملاحظات الإصدار Release Notes ودليل الترقية، فمثلاً في الإصدار 4.0 لم يعد الرمز * في valid_volumes يطابق المسارات المتداخلة Nested Paths وأصبح ذلك بالرمز **، وتغيرت أيضاً طريقة وصول المهام إلى ال cache. وإذا كنت تنتقل من act_runner القديم فاسم الـ Image أصبح gitea/runner بدلاً من gitea/act_runner، واسم الملف التنفيذي Binary أصبح gitea-runner.
وإذا كنت سوف تنقل Gitea نفسه إلى الإصدار 28.0 الذي صدر في 29 سبتمبر 2026، فاعلم أن الملفات في هذا الدليل تعمل معه كما هي، ولكنه يحذف تلقائياً التشغيلات المكتملة التي مضى عليها أكثر من 400 يوم عبر الإعداد الجديد RUN_RETENTION_DAYS، فإذا كنت تحتاج سجلاتها لفترة أطول فارفع القيمة أو اجعلها 0 قبل الترقية.
مشكلات شائعة وحلولها
التشغيل عالق على «Waiting» ولا يبدأ
السبب واحد من اثنين: لا يوجد runner يحمل الـ label المطلوب في runs-on، أو أن الـ runner غير متصل، لذلك قارن الـ labels في قائمة الـ runners بما كتبته في الـ workflow، وتذكر أن الـ runner المسجل على مستوى مستودع لا يرى المستودعات الأخرى.
فشل التسجيل: «unregistered runner» أو «connection refused»
قيمة GITEA_INSTANCE_URL يجب أن تطابق ROOT_URL، ولا تستخدم localhost، والسبب أنه داخل Container الـ runner يشير إلى الـ Container نفسه وليس إلى السيرفر، وتحقق أيضاً أن أحداً لم يقم بإعادة تعيين الرمز.
«exec: bash: executable file not found»
Image المهمة ليس فيها bash، وهذا حال الـ Images المبنية على Alpine، والحل أن تضيف defaults.run.shell: sh أو تستخدم الـ Image ubuntu-latest.
«http: server gave HTTP response to HTTPS client» عند docker login
Docker لا يقبل Registry يعمل عبر HTTP إلا إذا أعلنته «غير آمن»، والحل الصحيح هو تشغيل Gitea عبر HTTPS، أما في بيئة تجريبية معزولة فقط ومع الـ Image dind فتستطيع أن تمرر إلى Container الـ runner الملف /etc/docker/daemon.json وفيه {"insecure-registries": ["git.example.com:3000"]}.
المهمة لا تصل إلى git.example.com
Containers المهام تبحث عن الأسماء عبر DNS المعتاد، فإذا كان النطاق Domain داخلياً فقط فأضف عنوانه في container.options، مثل --add-host=git.example.com:203.0.113.10، والمفتاح موجود في مثال الإعداد الرسمي.
«413 Request Entity Too Large» أثناء docker push
سبب هذا الخطأ حد حجم الطلب في الـ Reverse Proxy الذي أمام Gitea، والحل أن ترفع هذا الحد، مثل client_max_body_size 1024m; في Nginx Proxy Manager، والسبب أن طبقات Layers الـ Images قد تتجاوز مئات الميغابايتات.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- Gitea Actions يبني الكود ويرفع الـ Images على سيرفرك، فلا يخرج الكود ولا الأسرار إلى خدمة خارجية، وملفاته بصيغة GitHub Actions تقريباً.
- Gitea لا ينفذ المهام بنفسه، وإنما يوزعها على Gitea Runner الذي كان اسمه act_runner، فسجله برمز من Site Administration واحفظ ال Volume الذي فيه ملف
.runner. - لا تمرر
docker.sockالخاص بالسيرفر إلى الـ runner، واستخدم النوع dind، وتذكر أنه يعزل المهام عن خدماتك ولكنه ليس حاجزاً أمام كود خبيث، فلا تشغل عليه إلا كوداً تثق به. - سجل الدخول في الـ Registry برمز بصلاحية
write:packageوحدها محفوظ في Secrets، وضع ما ليس سرياً في Variables. - ثبت كل action تجلبها من GitHub على هاش commit، وأضف قواعد تنظيف للحزم حتى لا يمتلئ القرص.
سجل التحديثات
- سبتمبر 2026: كتابة الدليل واختباره على Gitea 1.27.3 وGitea Runner 4.0.0.
- أكتوبر 2026: مراجعة الدليل، وتحديث Gitea Runner إلى الإصدار 4.1.0 واختباره مع Gitea 1.27.3 و28.0.0، وتصحيح تاريخ تغيير اسم act_runner، والاكتفاء بالصلاحية
write:packageللرمز، وإضافة ملاحظات الأمان عن الوضع privileged وطلبات الدمج من Fork وتثبيت ال actions.