العودة إلى المدونة

النشر التلقائي على خادم VPS باستخدام GitHub Actions وSSH

فريق تحرير IM Host25 أكتوبر 20267 دقائق قراءة
النشر التلقائي على خادم VPS باستخدام GitHub Actions وSSH

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

كيف تترابط الأجزاء

  1. تدفع الكود إلى الفرع main.

  2. يشغّل GitHub جهاز تنفيذ (runner) يجلب الكود ويشغّل الاختبارات.

  3. يتصل جهاز التنفيذ بالخادم عبر SSH باسم مستخدم deploy.

  4. إما أن يجلب الخادم الكود الجديد ويبنيه، أو يرفع جهاز التنفيذ نسخة جاهزة باستخدام rsync.

  5. يُعاد تشغيل التطبيق، ويتحقق سير العمل من أنه يستجيب.

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

الخطوة 1: أنشئ مستخدمًا للنشر

لا تنشر باستخدام root أو حسابك الشخصي. المستخدم المنفصل يملك مجلد التطبيق فقط.

sudo adduser --disabled-password --gecos "" deploy
sudo mkdir -p /var/www/app
sudo chown -R deploy:deploy /var/www/app

لا تضف هذا المستخدم إلى مجموعة sudo. إذا احتاج النشر إلى إعادة تشغيل خدمة، فاسمح بهذا الأمر وحده. عدّل القاعدة باستخدام visudo الذي يفحص الصياغة قبل الحفظ:

sudo visudo -f /etc/sudoers.d/deploy
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp

يجب أن يتطابق المسار تمامًا. إذا استدعى السكربت systemctl من مسار آخر أو بمعاملات مختلفة، فسيطلب sudo كلمة مرور وتفشل المهمة برسالة sudo: a terminal is required to read the password. وإذا كنت تشغّل التطبيق عبر PM2 تحت المستخدم deploy نفسه، فلا تحتاج إلى sudo إطلاقًا.

الخطوة 2: أنشئ مفتاحًا مخصصًا للنشر

أنشئ زوج مفاتيح جديدًا على جهازك. لا تعِد استخدام مفتاح SSH الشخصي، ولا تضع عبارة مرور، لأن جهاز التنفيذ لا يستطيع كتابتها.

ssh-keygen -t ed25519 -C "github-actions-deploy" -f ./deploy_key -N ""

ينتج هذا الأمر الملف deploy_key (المفتاح الخاص، ويذهب إلى GitHub) والملف deploy_key.pub (المفتاح العام، ويذهب إلى الخادم).

قيّد ما يستطيع المفتاح فعله

يسمح OpenSSH بكتابة خيارات قبل المفتاح في ملف authorized_keys. أهمها هنا هو الأمر الإجباري (forced command): مهما طلب العميل تنفيذه، لن ينفذ الخادم إلا السكربت الذي حددته.

sudo -u deploy mkdir -p /home/deploy/.ssh
sudo -u deploy nano /home/deploy/.ssh/authorized_keys
command="/var/www/app/deploy.sh",restrict ssh-ed25519 AAAAC3Nza... github-actions-deploy
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys

الخيار restrict يعطّل تمرير المنافذ وتمرير الوكيل (agent) وX11 وتخصيص الطرفية. والخيار command= يعني أن المفتاح المسروق لا يستطيع إلا بدء عملية نشر. يمكنك إضافة from="..." لتقييد عناوين المصدر، لكن أجهزة التنفيذ التي يستضيفها GitHub تستخدم نطاقات عناوين واسعة ومتغيرة، لذا لا يكون هذا عمليًا إلا مع جهاز تنفيذ مستضاف ذاتيًا.

لا يعمل الأمر الإجباري مع rsync، لأن rsync يحتاج إلى تشغيل عمليته الخاصة على الخادم. إذا اخترت طريقة rsync أدناه، فاستخدم rrsync بدلًا من ذلك، وهو يحصر المفتاح في مجلد واحد:

command="rrsync /var/www/app/releases",restrict ssh-ed25519 AAAAC3Nza... github-actions-upload

في الإصدارات الحديثة من Debian وUbuntu يُثبَّت rrsync مع حزمة rsync. أما في الإصدارات الأقدم فيأتي كسكربت داخل /usr/share/doc/rsync/scripts/.

الخطوة 3: احفظ الأسرار في GitHub

في المستودع، افتح Settings → Secrets and variables → Actions وأضف:

  • SSH_PRIVATE_KEY: محتوى الملف deploy_key كاملًا، بما في ذلك سطرا BEGIN وEND.

  • SSH_HOST وSSH_USER: عنوان الخادم واسم المستخدم deploy.

  • SSH_KNOWN_HOSTS: مفتاح المضيف الخاص بالخادم، حتى يتحقق جهاز التنفيذ من أنه يتصل بخادمك فعلًا.

احصل على مفتاح المضيف وقارن بصمته بالبصمة الموجودة على الخادم:

# from your machine
ssh-keyscan -t ed25519 203.0.113.10
# on the server, compare with
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

لا تستخدم StrictHostKeyChecking=no للتخلص من رسالة الخطأ، فهذا يلغي الحماية من الاتصال بجهاز خاطئ. واحذف الملف المحلي deploy_key بعد حفظ السر.

لتحكم إضافي، أنشئ بيئة (Environment) باسم production من Settings → Environments، وانقل الأسرار إليها وأضف مراجعين مطلوبين. عندها تنتظر عمليات النشر الموافقة. لاحظ أن الأسرار لا تُمرَّر إلى سير العمل الذي تطلقه طلبات الدمج القادمة من نسخ متفرعة (forks).

الخطوة 4، الخيار A: الخادم يجلب الكود ويبنيه

هذا أبسط إعداد، ويعمل جيدًا مع الأمر الإجباري. يحتاج الخادم إلى صلاحية قراءة المستودع، لذا أضف مفتاحًا ثانيًا تنشئه على الخادم كـ Deploy key للقراءة فقط من إعدادات المستودع.

السكربت الذي ينفذه الأمر الإجباري، /var/www/app/deploy.sh:

#!/usr/bin/env bash
set -euo pipefail
exec 9>/tmp/deploy.lock
flock -n 9 || { echo "deploy already running"; exit 1; }

cd /var/www/app/repo
git rev-parse HEAD > /var/www/app/PREVIOUS_SHA
git fetch --prune origin
git reset --hard origin/main
npm ci
npm run build
sudo /usr/bin/systemctl restart myapp
echo "deployed $(git rev-parse --short HEAD)"
chmod +x /var/www/app/deploy.sh

يمنع القفل تشغيل عمليتي نشر في الوقت نفسه، وحفظ رقم الـ commit السابق يسهّل التراجع. وهذا ملف سير العمل .github/workflows/deploy.yml:

name: Deploy
on:
  push:
    branches: [main]
  workflow_dispatch:

concurrency:
  group: production-deploy
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm test

      - name: Configure SSH
        env:
          SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_KEY" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          echo "$KNOWN_HOSTS" > ~/.ssh/known_hosts

      - name: Deploy
        run: ssh -i ~/.ssh/deploy_key ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}

      - name: Health check
        run: curl -fsS --retry 5 --retry-delay 3 https://example.com/health

تمرير الأسرار عبر env بدلًا من لصقها في سطر run يبقيها خارج نص الأمر، كما يخفي GitHub قيم الأسرار في السجلات.

الخطوة 4، الخيار B: البناء على جهاز التنفيذ والرفع عبر rsync

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

      - run: npm run build
      - name: Upload release
        run: |
          rsync -az -e "ssh -i ~/.ssh/deploy_key" dist/ ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ github.sha }}/

لأن rrsync يحصر المفتاح في /var/www/app/releases، يكون المسار البعيد نسبيًا إلى هذا المجلد، وكل إصدار يُرفع إلى مجلد يحمل رقم الـ commit. للتبديل إليه، استخدم مفتاحًا ثانيًا يوجّه أمره الإجباري الرابط الرمزي current إلى أحدث إصدار ويعيد تشغيل التطبيق. مفتاحان لكل منهما مهمة واحدة أسهل في الفهم من مفتاح واحد يستطيع فعل كل شيء.

التراجع: خطط له قبل أن تحتاجه

  • الخيار A: حفظ السكربت الملف PREVIOUS_SHA. التراجع يعني تنفيذ git reset --hard $(cat /var/www/app/PREVIOUS_SHA) ثم إعادة البناء والتشغيل. ضع ذلك في سكربت ثانٍ حتى لا تكتبه تحت الضغط.

  • الخيار B: الإصدارات القديمة ما زالت على القرص. وجّه current إلى المجلد السابق وأعد التشغيل. يستغرق هذا ثوانيَ لأنه لا يعيد البناء.

  • تلقائيًا: إذا فشلت خطوة فحص الصحة، يمكن لخطوة تالية بالشرط if: failure() أن تستدعي التراجع.

ترحيلات قاعدة البيانات هي الاستثناء. التراجع عن الكود لا يلغي الترحيل، لذا اكتب ترحيلات يستطيع الإصدار السابق من التطبيق العمل معها.

أخطاء شائعة

  • Permission denied (publickey): المفتاح العام ليس في ملف authorized_keys الصحيح، أو صلاحيات مجلد .ssh مفتوحة أكثر من اللازم. راجع /var/log/auth.log على الخادم.

  • Load key "/home/runner/.ssh/deploy_key": error in libcrypto: لُصق السر ناقصًا أو فقد فواصل الأسطر. الصق الملف كاملًا مرة أخرى.

  • Host key verification failed.: السر SSH_KNOWN_HOSTS فارغ أو لا يطابق. يحدث هذا أيضًا بعد إعادة تثبيت الخادم لأن مفتاح المضيف يتغير.

  • rsync: connection unexpectedly closed: الأداة rsync غير مثبتة على الخادم، أو يمنعها أمر إجباري غير rrsync.

هل يبقى منفذ SSH مفتوحًا على الإنترنت؟

الأمر يعتمد على وضعك. أجهزة التنفيذ التي يستضيفها GitHub تتصل من عناوين متغيرة، فلا يسهل السماح لها وحدها. الخياران الشائعان: SSH بالمفاتيح فقط مع تعطيل الدخول بكلمة مرور واستخدام أداة مثل fail2ban، أو جهاز تنفيذ مستضاف ذاتيًا داخل شبكتك بحيث يقتصر SSH على العناوين الخاصة. كلاهما معقول، فاختر ما يستطيع فريقك صيانته.

شغّله على IM HOST

تبدأ خطط Linux VPS من 5 دولارات شهريًا مع صلاحيات root كاملة على KVM، فتستطيع إنشاء المستخدمين وتعديل sudoers وتأمين SSH كما شرحنا هنا. وإذا كنت تبني على الخادم، فخطط Cloud VPS تمنحك vCPU وذاكرة مخصصة بدءًا من 16 دولارًا شهريًا، مع نسخة احتياطية تلقائية مجانية كل 3 أيام. لا توجد رسوم إعداد، ومهندسو الدعم متاحون على مدار الساعة، وإذا كان تطبيقك مستضافًا في مكان آخر اليوم، فإن خدمة النقل المجاني تنقل الملفات وقواعد البيانات دون توقف.

المزيد من مدونتنا

اكتشف المزيد من الأدلة العملية ورؤى الخدمات من فريق IM Host.

عرض كل المقالات