العودة للمدونة
devopsنُشر في August 14, 2026

إعداد سيرفر Ubuntu 24.04 للإنتاج: دليل كامل لمشاريع Laravel

دليل عملي من عشر مراحل لتجهيز سيرفر Ubuntu 24.04 من الصفر ونشر تطبيق Laravel عليه: Nginx و PHP 8.4 و MySQL و Redis و Supervisor و SSL عبر Certbot، مع عزل التطبيقات وجدولة المهام والنشر التلقائي عبر GitHub Actions وضبط OPcache.

إعداد سيرفر Ubuntu 24.04 للإنتاج: دليل كامل لمشاريع Laravel

إعداد سيرفر Ubuntu 24.04 للإنتاج

إعداد احترافي وقابل لإعادة الاستخدام لأي مشروع Laravel في بيئة الإنتاج.

هذا هو الإعداد الذي أرجع إليه عند استلام VPS جديد. كتبته بعد تكرار العملية أكثر من مرة ونسيان تفاصيل صغيرة — امتداد PHP هنا، صلاحية هناك، أو Worker لم يُعد تشغيله بعد النشر. الهدف ليس جمع أكبر عدد من الأوامر، بل الوصول إلى إعداد يمكن فهمه وصيانته بعد أشهر.

هذا الدليل يفترض أنك تملك سيرفر VPS جديد ولديك صلاحية sudo.

قبل أن تبدأ: اختر مسار التنفيذ

المراحل مرقمة للرجوع إليها، لكن ترتيب التنفيذ العملي لأول تطبيق هو:

  1. جهّز السيرفر عبر المرحلة الأولى.
  2. نفّذ خطوات التطبيق في المرحلة الثانية حتى إعداد ملف .env وتوليد APP_KEY.
  3. أنشئ قاعدة البيانات ومستخدمها عبر المرحلة الرابعة.
  4. ارجع إلى المرحلة الثانية لإنشاء رابط التخزين وإكمال Nginx، ثم شغّل migrations في نهاية المرحلة الرابعة.
  5. إن كان السيرفر سيستضيف أكثر من تطبيق، نفّذ المرحلة الثالثة قبل إعداد Cron والـ Workers. لموقع واحد بسيط يمكنك الاستمرار بمستخدم deploy وPHP-FPM الافتراضي.
  6. أكمل المراحل من الخامسة إلى العاشرة بالترتيب.

في الأمثلة، myapp اسم بديل للتطبيق وyourdomain.com اسم بديل للنطاق. لا تنسَ استبدالهما قبل تنفيذ الأوامر.


المرحلة الأولى - تجهيز السيرفر

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

الخطوة 1 - تحديث النظام

أول خطوة دائماً: تحديث الحزم ثم إعادة التشغيل لضمان تحميل أحدث نواة (Kernel).

sudo apt update && sudo apt upgrade -y
sudo reboot

الخطوة 2 - الأدوات الأساسية

هذه الحزم هي القاعدة التي تعتمد عليها بقية المراحل، من إدارة المستودعات إلى أدوات المراقبة والحماية.

sudo apt install -y \
software-properties-common \
curl \
wget \
git \
zip \
unzip \
htop \
nano \
vim \
tree \
net-tools \
ca-certificates \
apt-transport-https \
lsb-release \
gnupg \
fail2ban \
ufw

الخطوة 3 - إضافة مستودع PHP

مستودع ondrej/php هو المصدر الذي يتيح تثبيت عدة إصدارات من PHP جنباً إلى جنب على نفس السيرفر.

sudo add-apt-repository ppa:ondrej/php -y
sudo apt update

الخطوة 4 - تثبيت Nginx

sudo apt install nginx -y

sudo systemctl enable nginx
sudo systemctl start nginx

الخطوة 5 - تثبيت PHP

سنثبّت ثلاثة إصدارات معاً، لأن المشاريع القديمة قد تحتاج 8.2 بينما المشاريع الجديدة تعمل على 8.4.

PHP 8.2

sudo apt install -y \
php8.2-fpm php8.2-cli php8.2-common \
php8.2-bcmath php8.2-bz2 php8.2-curl \
php8.2-gd php8.2-imagick php8.2-intl \
php8.2-mbstring php8.2-mysql \
php8.2-opcache php8.2-readline \
php8.2-redis php8.2-soap php8.2-xml \
php8.2-zip

PHP 8.3

sudo apt install -y \
php8.3-fpm php8.3-cli php8.3-common \
php8.3-bcmath php8.3-bz2 php8.3-curl \
php8.3-gd php8.3-imagick php8.3-intl \
php8.3-mbstring php8.3-mysql \
php8.3-opcache php8.3-readline \
php8.3-redis php8.3-soap php8.3-xml \
php8.3-zip

PHP 8.4

sudo apt install -y \
php8.4-fpm php8.4-cli php8.4-common \
php8.4-bcmath php8.4-bz2 php8.4-curl \
php8.4-gd php8.4-imagick php8.4-intl \
php8.4-mbstring php8.4-mysql \
php8.4-opcache php8.4-readline \
php8.4-redis php8.4-soap php8.4-xml \
php8.4-zip

الخطوة 6 - جعل PHP 8.4 الافتراضي

sudo update-alternatives --install /usr/bin/php php /usr/bin/php8.2 82
sudo update-alternatives --install /usr/bin/php php /usr/bin/php8.3 83
sudo update-alternatives --install /usr/bin/php php /usr/bin/php8.4 84

sudo update-alternatives --config php

اختر:

PHP 8.4

ملاحظة: هذا يغيّر إصدار PHP في سطر الأوامر (CLI) فقط. أما إصدار PHP الذي يخدم كل موقع فيتحدد من خلال fastcgi_pass في إعداد Nginx الخاص بالموقع.


الخطوة 7 - Composer

cd /tmp

curl -sS https://getcomposer.org/installer -o composer-setup.php

EXPECTED_CHECKSUM="$(curl -sS https://composer.github.io/installer.sig)"
ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"

if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
    >&2 echo 'ERROR: Invalid Composer installer checksum'
    rm composer-setup.php
    exit 1
fi

php composer-setup.php

sudo mv composer.phar /usr/local/bin/composer

rm composer-setup.php

composer --version

الخطوة 8 - Node.js 22 LTS

مطلوب لبناء الواجهات الأمامية عبر Vite و npm.

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -

sudo apt install -y nodejs

node -v
npm -v

الخطوة 9 - MySQL

sudo apt install mysql-server -y

sudo systemctl enable mysql

sudo systemctl start mysql

بعد التثبيت، شغّل سكربت MySQL التفاعلي لإزالة الإعدادات الافتراضية غير الآمنة:

sudo mysql_secure_installation

احذف المستخدمين المجهولين وقاعدة test وامنع دخول root عن بُعد. في هذه المرحلة يظل الدخول المحلي متاحاً عبر sudo mysql؛ سنضبط كلمة مرور root صراحةً في المرحلة الرابعة.


الخطوة 10 - Redis

يُستخدم في Laravel للكاش والجلسات والطوابير (Queues).

sudo apt install redis-server -y

sudo systemctl enable redis-server

sudo systemctl start redis-server

الخطوة 11 - Supervisor

مسؤول عن إبقاء عمليات queue:work تعمل باستمرار وإعادة تشغيلها تلقائياً.

sudo apt install supervisor -y

الخطوة 12 - Certbot

لإصدار شهادات SSL مجانية من Let's Encrypt وتجديدها تلقائياً.

sudo apt install certbot python3-certbot-nginx -y

الخطوة 13 - Firewall

مهم: افتح OpenSSH قبل تفعيل الجدار الناري، وإلا ستفقد الاتصال بالسيرفر.

sudo ufw allow OpenSSH

sudo ufw allow 'Nginx Full'

sudo ufw enable

الخطوة 14 - Fail2Ban

يحظر عناوين IP التي تحاول تخمين كلمة المرور بشكل متكرر.

sudo systemctl enable fail2ban

sudo systemctl start fail2ban

الخطوة 15 - هيكل المشاريع

sudo mkdir -p /var/www

cd /var/www

sudo mkdir myapp

الخطوة 16 - مستخدم للنشر

لا تنشر مشاريعك باستخدام root. أنشئ مستخدماً مخصصاً للنشر وأضفه إلى مجموعة www-data.

sudo adduser deploy

sudo usermod -aG www-data deploy

الخطوة 17 - صلاحيات المشاريع

sudo chown deploy:www-data /var/www

sudo chmod 2775 /var/www

الخطوة 18 - Git

git --version

عمليات النشر التي تسحب الكود فقط لا تحتاج اسم مؤلف أو بريداً في Git. لا تضبطهما إلا إذا كان السيرفر سينشئ commits، واضبطهما بهوية المستخدم الذي سينشئها.


الخطوة 19 - التأكد

خطوة أخيرة سريعة للتحقق من أن كل شيء مثبّت ويعمل:

nginx -v

php -v

php8.2 -v

php8.3 -v

php8.4 -v

mysql --version

redis-server --version

composer --version

node -v

npm -v

هيكل السيرفر النهائي

Ubuntu 24.04
│
├── Nginx
├── PHP 8.2
├── PHP 8.3
├── PHP 8.4
├── Composer
├── Node.js 22 LTS
├── MySQL 8
├── Redis
├── Supervisor
├── Certbot
├── UFW
├── Fail2Ban
│
└── /var/www
    └── myapp

المرحلة الثانية - تثبيت تطبيق Laravel (المسار الأساسي)

هذا المسار مناسب لسيرفر يستضيف تطبيقاً واحداً. نفّذ الخطوات بالمستخدم deploy وليس root. إذا كنت ستستضيف عدة تطبيقات، أكمل هذا القسم أولاً ثم انقل التطبيق إلى مستخدمه المستقل في المرحلة الثالثة.

الخطوة 1 - إنشاء مفتاح SSH

هذا المفتاح هو ما سيستخدمه السيرفر للمصادقة مع GitHub عند سحب المشروع:

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""

الخطوة 2 - عرض المفتاح العام

cat ~/.ssh/id_ed25519.pub

انسخ الناتج وأضفه في مستودعك على GitHub تحت:

Settings → Deploy keys → Add deploy key

استخدم Deploy Key خاصاً بالمستودع بدل إضافة المفتاح إلى حسابك الشخصي، حتى لا يحصل السيرفر على صلاحية الوصول إلى كل مستودعاتك.

الخطوة 3 - منح المستخدم ملكية مجلد المشروع

sudo chown deploy:deploy /var/www

الخطوة 4 - سحب المشروع من GitHub

cd /var/www

git clone git@github.com:username/repository.git myapp

cd myapp

الخطوة 5 - تثبيت اعتماديات Composer

في بيئة الإنتاج نتجاهل حزم التطوير ونفعّل تحسين الـ autoloader:

composer install --no-dev --optimize-autoloader

الخطوة 6 - إعداد ملف البيئة

cp .env.example .env

nano .env

اضبط APP_ENV=production و APP_DEBUG=false وبيانات قاعدة البيانات.

الخطوة 7 - توليد مفتاح التطبيق

php artisan key:generate

أنشئ رابط التخزين الآن، وشغّل الهجرات لاحقاً بعد إنشاء قاعدة البيانات في المرحلة الرابعة:

php artisan storage:link

الخطوة 8 - ضبط ملكية التطبيق

أبقِ ملكية ملفات المصدر للمستخدم deploy حتى تستمر عمليات النشر بالعمل. يحتاج PHP-FPM إلى الكتابة في مجلدي التشغيل الخاصين بـ Laravel فقط:

sudo chown -R deploy:www-data /var/www/myapp
sudo chown -R www-data:www-data /var/www/myapp/storage /var/www/myapp/bootstrap/cache
sudo find /var/www/myapp/storage /var/www/myapp/bootstrap/cache -type d -exec chmod 775 {} \;
sudo find /var/www/myapp/storage /var/www/myapp/bootstrap/cache -type f -exec chmod 664 {} \;

لا تجعل المشروع كاملاً قابلاً للكتابة بواسطة مستخدم خادم الويب.

الخطوة 9 - إنشاء إعداد Nginx

sudo nano /etc/nginx/sites-available/myapp

والصق فيه الإعداد التالي:

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/myapp/public;

    index index.php;
    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
        fastcgi_index index.php;
        include fastcgi.conf;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

لاحظ أن fastcgi_pass هو المكان الذي تحدد فيه إصدار PHP لهذا الموقع تحديداً.

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/

الخطوة 11 - حذف الموقع الافتراضي

sudo rm /etc/nginx/sites-enabled/default

الخطوة 12 - اختبار الإعداد وإعادة تحميل Nginx

اختبر الإعداد أولاً حتى لا تُسقِط الخدمة بسبب خطأ إملائي:

sudo nginx -t

sudo systemctl reload nginx

الخطوة 13 - تفعيل SSL

أجّل إصدار الشهادة إلى المرحلة الثامنة، بعد اكتمال إعدادات النطاق وكتلة Nginx الافتراضية.


المرحلة الثالثة - عزل التطبيقات

حتى الآن، كل شيء يعمل تحت المستخدم www-data داخل /var/www. هذا مقبول لموقع واحد، لكنه خطر عندما تستضيف عدة مشاريع على نفس السيرفر: أي ثغرة في أحد المواقع تعني وصولاً كاملاً إلى ملفات كل المواقع الأخرى، لأنها جميعاً مملوكة لنفس المستخدم.

الحل هو عزل كل تطبيق في مستخدم خاص به، وتشغيل PHP-FPM بهوية ذلك المستخدم. سنستخدم هنا اسم المشروع myapp كمثال — استبدله باسم مشروعك.

الخطوة 1 - إنشاء مستخدم خاص بالتطبيق

sudo adduser myapp

الخطوة 2 - نقل ملفات التطبيق إلى مجلد المستخدم

بدل /var/www المشترك، يصبح لكل تطبيق مجلده داخل /home:

sudo mv /var/www/myapp /home/myapp/www

الخطوة 3 - تغيير ملكية المجلد

sudo chown -R myapp:myapp /home/myapp/www

الخطوة 4 - إضافة www-data إلى مجموعة التطبيق

Nginx يعمل تحت المستخدم www-data، ويحتاج صلاحية قراءة الملفات الثابتة داخل مجلد المشروع:

sudo usermod -aG myapp www-data

ثم تأكد أن www-data يستطيع المرور عبر مجلد المنزل:

sudo chmod 750 /home/myapp

الخطوة 5 - إنشاء PHP-FPM Pool مستقل

هذه هي الخطوة الجوهرية في العزل: نجعل عمليات PHP تعمل بهوية myapp بدل www-data.

لا تعدّل ملف www.conf المشترك، لأن ذلك سيجعل كل المواقع التي تستخدم الـ Pool الافتراضي تعمل بهوية مستخدم التطبيق نفسه. أنشئ Pool كاملاً خاصاً بالتطبيق:

sudo nano /etc/php/8.4/fpm/pool.d/myapp.conf
[myapp]
user = myapp
group = myapp
listen = /run/php/php8.4-fpm-myapp.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

chdir = /

اضبط pm.max_children بعد قياس استهلاك الذاكرة لكل عملية PHP؛ القيمة 10 نقطة بداية محافظة فقط.

الخطوة 6 - تحديث إعدادات Nginx

بعد نقل المشروع من /var/www/myapp إلى /home/myapp/www لن يعمل الموقع حتى تحدّث ملف Nginx. افتح ملف الموقع:

sudo nano /etc/nginx/sites-available/myapp

استبدل المسار القديم:

root /var/www/myapp/public;

بالمسار الجديد:

root /home/myapp/www/public;

ثم استبدل سوكت PHP-FPM الافتراضي:

fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;

بسوكت التطبيق المستقل:

fastcgi_pass unix:/run/php/php8.4-fpm-myapp.sock;

احفظ الملف. لا تحتاج إلى إنشاء ملف Nginx جديد أو إعادة إنشاء الـ symlink؛ عدّلت الملف المفعّل نفسه فقط.

الخطوة 7 - إعادة تشغيل الخدمات

اختبر إعداد Nginx أولاً، ثم أعد تحميل الخدمتين:

sudo nginx -t

sudo systemctl restart nginx

sudo systemctl restart php8.4-fpm

الخطوة 8 - النشر باستخدام مستخدم التطبيق

من الآن فصاعداً، سجّل الدخول بالمستخدم myapp عند تحديث المشروع:

cd /home/myapp/www

git pull --ff-only origin main

أنشئ مفتاح SSH جديداً للمستخدم myapp وأضفه كـ Deploy Key للمستودع؛ المفتاح السابق موجود في حساب deploy ولا ينتقل تلقائياً مع ملفات المشروع.

ما الذي يضيفه هذا العزل؟

  • ثغرة في تطبيق واحد لا تعطي المهاجم وصولاً إلى ملفات بقية التطبيقات.
  • كل تطبيق يملك ملفاته، فلا تحتاج إلى صلاحيات 777.
  • سجلات الأخطاء والعمليات تصبح واضحة: تعرف أي مستخدم يستهلك الموارد من خلال htop.

المرحلة الرابعة - إعداد قاعدة البيانات

ثبّتنا MySQL في الخطوة 9 من المرحلة الأولى، لكن التطبيق يحتاج قاعدة بيانات ومستخدماً خاصاً به. لا تستخدم مستخدم root في تطبيقك أبداً.

إن كنت تنفّذ الدليل بالترتيب، أنجز هذه المرحلة قبل تشغيل php artisan migrate في المرحلة الثانية، لأن الهجرات تحتاج قاعدة بيانات موجودة مسبقاً.

سبق تأمين MySQL بالأمر sudo mysql_secure_installation في المرحلة الأولى؛ لا تشغّله مرة ثانية هنا.

الخطوة 1 - الدخول إلى MySQL

على Ubuntu، يدخل مستخدم root عبر مصادقة النظام (auth_socket) دون كلمة مرور لقاعدة البيانات:

sudo mysql

الخطوة 2 - ضبط كلمة مرور root

ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'your_password';

بعد تنفيذ هذا الأمر تتغير مصادقة root من socket إلى كلمة مرور، ولذلك اخرج من MySQL بالأمر exit، ثم استخدم mysql -u root -p بدل sudo mysql في مرات الدخول اللاحقة.

الخطوة 3 - إنشاء قاعدة البيانات

CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

استخدم utf8mb4 وليس utf8. ترميز utf8 في MySQL يخزّن 3 بايتات فقط لكل حرف، فلا يدعم الإيموجي وبعض الرموز، وهو سبب شائع لخطأ Incorrect string value عند حفظ نصوص المستخدمين. و utf8mb4 هو الترميز الافتراضي في Laravel أصلاً.

الخطوة 4 - إنشاء مستخدم للتطبيق

CREATE USER 'myapp'@'localhost' IDENTIFIED BY 'your_password';

الخطوة 5 - منح الصلاحيات

نمنح المستخدم صلاحيات كاملة على قاعدة بياناته فقط، لا على كل قواعد البيانات:

GRANT ALL PRIVILEGES ON myapp.* TO 'myapp'@'localhost';

الخطوة 6 - تطبيق الصلاحيات

FLUSH PRIVILEGES;

ثم اخرج:

EXIT;

الخطوة 7 - التأكد من امتداد PHP الخاص بـ MySQL

ثبّتناه ضمن حزم PHP في المرحلة الأولى، وللتأكد:

php -m | grep mysql

وإن لم يظهر:

sudo apt install php8.4-mysql -y

sudo service php8.4-fpm restart

الخطوة 8 - ربط التطبيق بقاعدة البيانات

عدّل ملف .env في مشروعك:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=myapp
DB_PASSWORD=your_password

الخطوة 9 - تشغيل الهجرات

php artisan migrate --force

نستخدم --force في بيئة الإنتاج لأن Laravel يطلب تأكيداً تفاعلياً قبل تنفيذ الهجرات هناك.


المرحلة الخامسة - جدولة المهام

يعتمد Laravel على أمر واحد فقط في نظام التشغيل: schedule:run. يُستدعى كل دقيقة، ثم يقرر Laravel داخلياً أي مهام حان وقت تنفيذها بناءً على ما عرّفته في routes/console.php. لذلك لا تحتاج إلى إضافة سطر cron لكل مهمة.

من هذه المرحلة فصاعداً تفترض الأمثلة أنك اخترت العزل في المرحلة الثالثة، ولذلك تستخدم المستخدم myapp والمسار /home/myapp/www. إن بقيت على المسار الأساسي، استبدلهما بالمستخدم deploy والمسار /var/www/myapp.

الخطوة 1 - التبديل إلى مستخدم التطبيق

يجب أن يعمل الـ Cron بهوية المستخدم المالك للتطبيق، وإلا ستُنشأ ملفات السجلات والكاش بملكية خاطئة:

sudo -iu myapp

الخطوة 2 - فتح ملف الـ Crontab الخاص بالمستخدم

crontab -e

الخطوة 3 - إضافة المهمة

أضف هذا السطر في نهاية الملف:

* * * * * /usr/bin/php8.4 /home/myapp/www/artisan schedule:run >> /home/myapp/www/storage/logs/cron.log 2>&1

شرح مكوّنات السطر:

  • * * * * * — التنفيذ كل دقيقة، وهو التردد الوحيد الذي يحتاجه Laravel.
  • /usr/bin/php8.4 — المسار الكامل للمفسّر. بيئة الـ Cron لا تقرأ متغير PATH الخاص بك، لذا php وحدها قد تفشل أو تستدعي إصداراً مختلفاً.
  • >> ... cron.log — إلحاق المخرجات بملف السجل.
  • 2>&1 — توجيه رسائل الأخطاء إلى نفس الملف، وبدونها تضيع أخطاء التنفيذ بصمت.

احفظ الملف واخرج. للتأكد من أن المهمة سُجّلت:

crontab -l

الخطوة 4 - التحقق من العمل

انتظر دقيقة، ثم راقب ملف السجل:

tail -f /home/myapp/www/storage/logs/cron.log

ويمكنك اختبار الأمر يدوياً قبل ذلك:

php /home/myapp/www/artisan schedule:run

إن ظهر خطأ صلاحيات على مجلد storage، تأكد أن المستخدم myapp يملك المجلد كما ضبطنا في المرحلة الثالثة.

الخطوة 5 - تدوير ملف السجل

ملف cron.log ينمو بلا حدود وقد يملأ القرص مع الوقت. أنشئ إعداد تدوير له:

sudo nano /etc/logrotate.d/myapp-cron

والصق:

/home/myapp/www/storage/logs/cron.log {
    weekly
    rotate 4
    compress
    missingok
    notifempty
    create 0640 myapp myapp
}

بديل أبسط: استبدل مسار السجل في الـ Cron بـ >> /dev/null 2>&1 والاعتماد على سجلات Laravel نفسها، لكن حينها ستفقد رسائل الأخطاء التي تحدث قبل إقلاع التطبيق.


المرحلة السادسة - Redis والطوابير

إرسال البريد ومعالجة الصور والاتصال بخدمات خارجية أمثلة جيدة على الأعمال التي تُنقل إلى Queue. يخزّن Redis الطابور، تنفّذ الـ Workers المهام، ويتولى Supervisor إبقاء العمليات عاملة.

الخطوة 1 - التأكد من Redis

ثبّتنا Redis في الخطوة 10 من المرحلة الأولى، وللتأكد من عمله:

redis-cli ping

يجب أن يكون الرد:

PONG

الخطوة 2 - التأكد من امتداد Redis في PHP

php -m | grep redis

أو باختبار مباشر (لا يطبع شيئاً عند النجاح، ويرمي خطأ عند الفشل):

php -r "new Redis();"

وإن لم يكن مثبتاً:

sudo apt install php8.4-redis -y

sudo service php8.4-fpm restart

الخطوة 3 - ضبط Laravel لاستخدام Redis

في ملف .env:

QUEUE_CONNECTION=redis
CACHE_STORE=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379

تتضمن تطبيقات Laravel الجديدة عادةً migration لجدول failed_jobs. إن لم يكن موجوداً في مشروعك، أنشئه بالأمر الحالي ثم نفّذ الهجرات:

php artisan make:queue-failed-table

php artisan migrate --force

الخطوة 4 - إنشاء ملف إعداد Supervisor

ثبّتنا Supervisor في الخطوة 11 من المرحلة الأولى. ننتقل إلى مجلد الإعدادات وننشئ ملفاً للتطبيق:

cd /etc/supervisor/conf.d

sudo nano myapp-workers.conf

والصق فيه:

[program:myapp-workers]
process_name=%(program_name)s_%(process_num)02d
command=/usr/bin/php8.4 /home/myapp/www/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
directory=/home/myapp/www
user=myapp
numprocs=3
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
redirect_stderr=true
stdout_logfile=/home/myapp/www/storage/logs/workers.log
stopwaitsecs=3600

شرح أهم المفاتيح:

  • numprocs=3 — عدد العمّال المتوازين. ابدأ بعدد قليل وزِده حسب حجم الطابور وعدد أنوية المعالج.
  • user=myapp — يعمل العامل بهوية مالك التطبيق، تماشياً مع العزل في المرحلة الثالثة.
  • autorestart=true — إعادة التشغيل تلقائياً عند التعطل، وهو سبب وجود Supervisor أصلاً.
  • stopwaitsecs=3600 — يمنح المهمة الجارية وقتاً للانتهاء قبل أن يجبر Supervisor العملية على التوقف. اجعله أطول من مهلة أطول Job لديك.
  • --max-time=3600 — ينهي العامل نفسه كل ساعة ليعيد Supervisor تشغيله، وهي طريقة بسيطة لتفادي تسريبات الذاكرة.

الخطوة 5 - إنشاء ملف السجل وضبط ملكيته

sudo touch /home/myapp/www/storage/logs/workers.log

sudo chown myapp:myapp /home/myapp/www/storage/logs/workers.log

الخطوة 6 - تحميل الإعداد الجديد

sudo supervisorctl reread

sudo supervisorctl update
  • reread يقرأ ملفات الإعداد الجديدة.
  • update يشغّل البرامج المضافة أو المعدّلة فعلياً.

الخطوة 7 - التحقق من حالة العمّال

sudo supervisorctl status myapp-workers:*

يفترض أن ترى ثلاث عمليات بحالة RUNNING. ولمتابعة السجل:

tail -f /home/myapp/www/storage/logs/workers.log

الخطوة 8 - إعادة تشغيل العمّال بعد كل نشر

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

php artisan queue:restart

هذا الأمر يطلب من العمّال إنهاء المهمة الحالية ثم الخروج بلطف، ويتكفل Supervisor بإعادة تشغيلهم بالكود الجديد.


المرحلة السابعة - ضبط أسماء النطاقات

عندما يستقبل Nginx طلباً، يبحث عن كتلة server يطابق server_name فيها الدومين المطلوب. وإن لم يجد تطابقاً، يسلّم الطلب إلى الخادم الافتراضي (default_server). النتيجة العملية: أي دومين يشير إلى عنوان IP سيرفرك — حتى لو لم يكن ملكك — قد يعرض موقعك. هنا نضبط هذا السلوك.

الخطوة 1 - إزالة علامة default_server من موقعك

sudo nano /etc/nginx/sites-available/myapp

تأكد أن سطور listen لا تحتوي على الكلمة default_server:

listen 80;
listen [::]:80;

موقعك يجب أن يستجيب لدومينه فقط عبر server_name، لا أن يكون هو الملاذ الأخير لكل طلب مجهول.

الخطوة 2 - إنشاء خادم افتراضي يبتلع الطلبات المجهولة

sudo nano /etc/nginx/sites-available/default

والصق:

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    server_name _;

    return 444;
}
  • default_server — هذه الكتلة تستقبل كل طلب لا يطابق أي دومين معرّف.
  • server_name _ — اسم وهمي لا يطابق أي دومين حقيقي.
  • return 444 — رمز خاص بـ Nginx يغلق الاتصال دون أي رد. بديله return 404 إن كنت تفضّل رداً صريحاً.

الخطوة 3 - تفعيل الخادم الافتراضي

sudo ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/

sudo nginx -t

sudo service nginx reload

إن كنت قد حذفت الرابط الافتراضي في المرحلة الثانية، فهذه الخطوة تعيده لكن بمحتوى نحن من كتبناه.

الخطوة 4 - توحيد النطاق: من www إلى بدون www

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

افتح إعداد موقعك:

sudo nano /etc/nginx/sites-available/myapp

واحذف www.yourdomain.com من سطر server_name في الكتلة الأساسية، ليصبح:

server_name yourdomain.com;

ثم أضف كتلة تحويل مستقلة في نفس الملف:

server {
    listen 80;
    listen [::]:80;

    server_name www.yourdomain.com;

    return 301 http://yourdomain.com$request_uri;
}

لاحظ $request_uri في نهاية العنوان. بدونها سيُحوَّل زائر الصفحة www.yourdomain.com/blog/post إلى الصفحة الرئيسية ويفقد وجهته.

الخطوة 5 - إعادة التحميل والتأكد

sudo nginx -t

sudo service nginx reload

اختبر التحويل:

curl -I http://www.yourdomain.com

يجب أن ترى:

HTTP/1.1 301 Moved Permanently
Location: http://yourdomain.com/

بعد تفعيل SSL عبر Certbot، سيعدّل الأخير هذه الكتل تلقائياً ليصبح التحويل إلى https://. لذا شغّل certbot --nginx على الدومينين معاً حتى تغطي الشهادة صيغة www أيضاً.


المرحلة الثامنة - تفعيل SSL

بعد أن استقر إعداد Nginx على منفذ 80، حان وقت تشفير الاتصال. سنستخدم Certbot الذي ثبّتناه في الخطوة 12 من المرحلة الأولى، وهو يتولى إصدار الشهادة وتعديل إعدادات Nginx وتجديد الشهادة تلقائياً.

قاعدة أساسية: لا تكتب كتل listen 443 ssl يدوياً قبل تشغيل Certbot. اترك إعدادك يعمل على HTTP أولاً، ودع Certbot هو من يضيف إعدادات SSL. أي كتلة 443 مكتوبة يدوياً وناقصة ستربك Certbot وقد تمنعه من إتمام العملية.

الخطوة 1 - التأكد من سجلات DNS

قبل أي شيء، يجب أن يشير كل دومين تريد تأمينه إلى عنوان IP سيرفرك عبر سجل A. تتحقق Let's Encrypt من ملكيتك للدومين بزيارته فعلياً، فإن لم يكن DNS جاهزاً ستفشل العملية.

dig +short yourdomain.com

dig +short www.yourdomain.com

يجب أن يكون الناتج هو عنوان IP سيرفرك.

الخطوة 2 - التأكد من إعداد Nginx

sudo nginx -t

sudo systemctl reload nginx

تأكد أيضاً أن Nginx Full مسموح في الجدار الناري (ضبطناه في الخطوة 13 من المرحلة الأولى)، لأن منفذ 443 يجب أن يكون مفتوحاً.

الخطوة 3 - إصدار الشهادة

للدومين الرئيسي مع صيغة www في شهادة واحدة:

sudo certbot --nginx \
-d yourdomain.com \
-d www.yourdomain.com

ولأي نطاق فرعي مستقل (لوحة تحكم مثلاً) له ملف إعداد منفصل في Nginx:

sudo certbot --nginx -d admin.yourdomain.com

ضع كل الدومينات التي يخدمها ملف إعداد واحد في أمر certbot واحد. أما النطاقات التي لها ملفات إعداد منفصلة فتحتاج أوامر منفصلة.

الخطوة 4 - اختيار التحويل إلى HTTPS

سيسألك Certbot عمّا إذا كنت تريد تحويل طلبات HTTP إلى HTTPS. اختر خيار التحويل (Redirect). عندها يضيف Certbot تلقائياً:

  • كتل listen 443 ssl مع مسارات الشهادة والمفتاح.
  • تحويل 301 من منفذ 80 إلى HTTPS.

وستصبح مواقعك متاحة على:

https://yourdomain.com
https://www.yourdomain.com
https://admin.yourdomain.com

الخطوة 5 - التحقق من الشهادات

sudo nginx -t

sudo systemctl reload nginx

sudo certbot certificates

يعرض الأمر الأخير كل شهادة مع الدومينات التي تغطيها وتاريخ انتهائها.

الخطوة 6 - اختبار التجديد التلقائي

شهادات Let's Encrypt صالحة 90 يوماً فقط، لذا التجديد التلقائي ليس رفاهية. اختبره دون إصدار شهادة فعلية:

sudo certbot renew --dry-run

الخطوة 7 - التأكد من مؤقّت التجديد

يثبّت Certbot مؤقّت systemd يفحص الشهادات مرتين يومياً ويجدد ما اقترب انتهاؤه:

sudo systemctl status certbot.timer

يجب أن تكون حالته active (waiting). ولمعرفة موعد التشغيل القادم:

systemctl list-timers certbot.timer

إن كنت قد أضفت كتلة default_server في المرحلة السابعة، فتأكد أنها لا تعترض مسار التحقق /.well-known/acme-challenge/، وإلا سيفشل التجديد بصمت بعد 90 يوماً — وهو خطأ لن تكتشفه إلا عند توقف الموقع.


المرحلة التاسعة - النشر التلقائي

آخر خطوة: أن يصبح git push هو كل ما تفعله للنشر. سنمنح GitHub Actions مفتاح SSH خاصاً بالدخول إلى السيرفر، وننشئ سكربت نشر يعمل عند كل دفعة إلى الفرع الرئيسي.

الخطوة 1 - إنشاء زوج مفاتيح للنشر

على جهازك المحلي، أنشئ مفتاحاً مخصصاً لـ GitHub Actions ولا تستخدم مفتاحك الشخصي:

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

ينتج ملفان: github-actions (المفتاح الخاص) و github-actions.pub (المفتاح العام).

تحذير مهم: لا تنشئ المفتاح داخل مجلد المشروع (مثل ./storage/key) ولا تضعه في مستودع Git أبداً. مفتاح خاص مسرَّب في المستودع يعني وصولاً كاملاً إلى سيرفرك. أنشئه في مجلد مؤقت خارج المشروع، واحذفه من جهازك بعد إضافته إلى GitHub.

الخطوة 2 - نسخ المفتاح العام إلى السيرفر

اعرض المفتاح العام:

cat github-actions.pub

ثم أضفه إلى ملف authorized_keys الخاص بمستخدم التطبيق على السيرفر:

sudo -iu myapp

mkdir -p ~/.ssh

nano ~/.ssh/authorized_keys

الصق المفتاح في سطر جديد، ثم اضبط الصلاحيات:

chmod 700 ~/.ssh

chmod 600 ~/.ssh/authorized_keys

قد يرفض SSH المفتاح إذا كانت صلاحيات ملفاته متساهلة. استخدم 700 للمجلد و600 للملف.

الخطوة 3 - اختبار الاتصال

من جهازك المحلي:

ssh -i ./github-actions myapp@YOUR_SERVER_IP

يجب أن تدخل مباشرة دون كلمة مرور. إن نجح هذا، سينجح GitHub Actions.

الخطوة 4 - إنشاء سكربت النشر على السيرفر

بدل وضع كل الأوامر داخل ملف الـ Workflow، ضعها في سكربت على السيرفر — يصبح تعديل خطوات النشر لاحقاً دون تعديل المستودع.

nano /home/myapp/deploy.sh

والصق:

#!/bin/bash
set -e

cd /home/myapp/www

git pull --ff-only origin main

composer install --no-dev --optimize-autoloader --no-interaction

php artisan migrate --force

php artisan config:cache
php artisan route:cache
php artisan view:cache

npm ci
npm run build

php artisan queue:restart

ثم اجعله قابلاً للتنفيذ:

chmod +x /home/myapp/deploy.sh
  • set -e — يوقف السكربت عند أول خطأ، فلا يكمل النشر على حالة نصف منتهية.
  • queue:restart — ضروري ليرى العمّال الكود الجديد، كما شرحنا في المرحلة السادسة.
  • أوامر الـ Cache تُشغَّل بعد سحب الكود، لأنها تخزّن الإعدادات والمسارات الحالية.

الخطوة 5 - إضافة الأسرار في GitHub

في مستودعك على GitHub:

Settings → Secrets and variables → Actions → New repository secret

أضف أربعة أسرار:

  • SSH_PRIVATE_KEY — محتوى ملف github-actions كاملاً (شاملاً سطري البداية والنهاية).
  • SSH_HOST — عنوان IP سيرفرك.
  • SSH_USER — اسم مستخدم التطبيق، أي myapp.
  • SSH_FINGERPRINT — بصمة مفتاح المضيف ED25519 بصيغة SHA-256. استخرجها من لوحة تحكم السيرفر بالأمر ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub -E sha256 وانسخ قيمة SHA256:... فقط.

الخطوة 6 - إنشاء ملف الـ Workflow

في مشروعك، أنشئ الملف .github/workflows/deploy.yml:

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@v1.2.2
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          fingerprint: ${{ secrets.SSH_FINGERPRINT }}
          script: bash /home/myapp/deploy.sh

الخطوة 7 - التجربة

ادفع أي تعديل إلى الفرع الرئيسي:

git push origin main

ثم تابع التنفيذ من تبويب Actions في مستودعك. عند أول فشل، ستجد مخرجات السكربت كاملة في سجل الخطوة.

ابدأ بتشغيل bash /home/myapp/deploy.sh يدوياً على السيرفر مرة واحدة قبل ربطه بـ GitHub. هكذا تفصل بين أخطاء السكربت وأخطاء الاتصال، بدل مطاردة السببين معاً.


المرحلة العاشرة - ضبط OPcache

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

الخطوة 1 - فتح ملف الإعدادات

sudo nano /etc/php/8.4/fpm/php.ini

الخطوة 2 - ضبط قيم OPcache

ابحث عن قسم [opcache] واضبط القيم التالية:

opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.save_comments=1

شرح ما يهم منها:

  • memory_consumption=256 — نقطة بداية مناسبة لتطبيق متوسط، وليست رقماً ثابتاً لكل سيرفر. راقب الذاكرة الحرة وعدّلها حسب حجم المشروع والـ RAM المتاح.
  • max_accelerated_files=20000 — عدد الملفات المخزّنة. مشروع Laravel مع اعتمادياته يتجاوز 10 آلاف ملف بسهولة، وتجاوز الحد يعني إسقاط ملفات من الكاش.
  • save_comments=1 — اترك التعليقات محفوظة لتوافق الحزم التي تقرأ metadata من docblocks.
  • validate_timestamps=0 — أهم قيمة، وأخطرها. اقرأ الفقرة التالية قبل ضبطها.

الخطوة 3 - فهم validate_timestamps

عندما تكون validate_timestamps=1 (الافتراضي)، تفحص PHP تاريخ تعديل كل ملف في كل طلب لتعرف إن تغيّر. هذا مريح في التطوير لكنه هدر في الإنتاج.

وعندما تجعلها 0، تتوقف PHP عن الفحص نهائياً — أسرع، لكن لن يرى السيرفر أي تعديل تنشره حتى تُفرغ الكاش يدوياً. لذلك:

إن ضبطت validate_timestamps=0، فإعادة تحميل PHP-FPM بعد كل نشر لم تعد اختياراً بل شرطاً. من دونها سيظل موقعك يعرض الكود القديم بعد النشر، وهي مشكلة محيّرة يضيع فيها وقت طويل.

بديل وسط إن أردت تفادي هذا التعقيد:

opcache.validate_timestamps=1
opcache.revalidate_freq=60

أي: افحص التغييرات مرة كل 60 ثانية فقط، بدل كل طلب.

الخطوة 4 - إعادة تشغيل PHP-FPM

sudo service php8.4-fpm restart

الفرق بين restart و reload: الأول يُنهي كل العمليات ويعيد تشغيلها، فيقطع الطلبات الجارية للحظة. أما reload فيعيد تحميل الإعدادات ويفرّغ OPcache بلطف دون قطع الخدمة، وهو المناسب بعد كل عملية نشر.

الخطوة 5 - السماح لمستخدم التطبيق بإعادة التحميل

سكربت النشر يعمل بهوية myapp، وهو مستخدم بلا صلاحيات sudo. نمنحه استثناءً ضيقاً لأمر واحد فقط:

sudo visudo

وأضف في نهاية الملف:

myapp ALL=(ALL) NOPASSWD: /usr/sbin/service php8.4-fpm reload

انتبه إلى دقة المسار والأمر. لا تمنح NOPASSWD لأمر عام مثل /usr/sbin/service بلا تقييد، لأن ذلك يعني عملياً منح المستخدم القدرة على التحكم بكل خدمات النظام.

ثم أضف السطر التالي إلى نهاية deploy.sh الذي أنشأناه في المرحلة التاسعة:

sudo service php8.4-fpm reload

الخطوة 6 - التحقق من عمل OPcache

من داخل التطبيق (مثلاً في routes/web.php مؤقتاً):

dd(opcache_get_status());

أو من سطر الأوامر مباشرة:

sudo php-fpm8.4 -i | grep opcache.enable

في ناتج opcache_get_status() راقب تحديداً:

  • opcache_enabled — يجب أن يكون true.
  • memory_usage.free_memory — إن اقترب من الصفر، فالذاكرة المخصصة غير كافية.
  • opcache_statistics.opcache_hit_rate — يجب أن يتجاوز 99% في الإنتاج. النسبة المنخفضة تعني أن الكاش يُفرَّغ باستمرار.
  • opcache_statistics.num_cached_scripts — إن اقترب من max_accelerated_files، ارفع الحد.

احذف مسار الفحص المؤقت فور الانتهاء لأنه يكشف معلومات عن الخادم. إعدادات php.ini الخاصة بـ FPM لا تنطبق على CLI، ولهذا استخدمنا php-fpm8.4 -i بدلاً من php -i.


الخطوات التالية

بعد الانتهاء من إعداد السيرفر ونشر التطبيق، ننتقل إلى:

تأمين السيرفر

يشمل:

  • الدخول بمفاتيح SSH فقط وتعطيل المصادقة بكلمة المرور.
  • تعطيل تسجيل الدخول باستخدام root.
  • تغيير منفذ SSH.
  • ضبط قواعد Fail2Ban.
  • ضبط قواعد UFW.
  • تحسين إعدادات الأمان الأساسية.

النسخ الاحتياطي والمراقبة

  • جدولة نسخ احتياطية لقاعدة البيانات والملفات.
  • مراقبة السجلات (Logs) ومساحة القرص.

الخلاصة

الفائدة الحقيقية من هذا الدليل ليست في الأوامر نفسها، بل في كونه معياراً ثابتاً: كل سيرفر تجهزه بنفس الطريقة، فتصبح مشاكل النشر متوقعة وقابلة للحل بسرعة، وينتقل أي مشروع بين السيرفرات دون مفاجآت.

احتفظ بهذه المراحل كمرجع، وحدّث أرقام الإصدارات مع كل نسخة جديدة من Ubuntu أو PHP.

#VPS #Ubuntu 24.04 #Laravel #Nginx #PHP 8.4 #Composer #Node.js 22 #MySQL #Redis #Supervisor #Certbot #SSL #UFW #Fail2Ban #GitHub Actions #OPcache #نشر تلقائي #إعداد سيرفر #DevOps