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

إعداد سيرفر Ubuntu 24.04 للإنتاج
إعداد احترافي وقابل لإعادة الاستخدام لأي مشروع Laravel في بيئة الإنتاج.
هذا هو الإعداد الذي أرجع إليه عند استلام VPS جديد. كتبته بعد تكرار العملية أكثر من مرة ونسيان تفاصيل صغيرة — امتداد PHP هنا، صلاحية هناك، أو Worker لم يُعد تشغيله بعد النشر. الهدف ليس جمع أكبر عدد من الأوامر، بل الوصول إلى إعداد يمكن فهمه وصيانته بعد أشهر.
هذا الدليل يفترض أنك تملك سيرفر VPS جديد ولديك صلاحية sudo.
قبل أن تبدأ: اختر مسار التنفيذ
المراحل مرقمة للرجوع إليها، لكن ترتيب التنفيذ العملي لأول تطبيق هو:
- جهّز السيرفر عبر المرحلة الأولى.
- نفّذ خطوات التطبيق في المرحلة الثانية حتى إعداد ملف
.envوتوليدAPP_KEY. - أنشئ قاعدة البيانات ومستخدمها عبر المرحلة الرابعة.
- ارجع إلى المرحلة الثانية لإنشاء رابط التخزين وإكمال Nginx، ثم شغّل migrations في نهاية المرحلة الرابعة.
- إن كان السيرفر سيستضيف أكثر من تطبيق، نفّذ المرحلة الثالثة قبل إعداد Cron والـ Workers. لموقع واحد بسيط يمكنك الاستمرار بمستخدم
deployوPHP-FPM الافتراضي. - أكمل المراحل من الخامسة إلى العاشرة بالترتيب.
في الأمثلة، 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 لهذا الموقع تحديداً.
الخطوة 10 - تفعيل الموقع عبر Symlink
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.