من خادم VPS فارغ إلى تطبيق في بيئة الإنتاج
Docker ومنصات PaaS عملية، لكن خادم VPS مضبوطاً بعناية يبقى خياراً متيناً واقتصادياً وتحت سيطرتك الكاملة لتشغيل تطبيق Symfony. ينطلق هذا الدليل من خادم Ubuntu 24.04 LTS مثبَّت حديثاً، وينتهي بتطبيق يُقدَّم عبر HTTPS مع عمّاله ومهامه المجدولة.
التأمين الأساسي (مستخدم غير root، مفاتيح SSH، جدار ناري، fail2ban) مشروح في مقال تأمين خادم Linux: ابدأ به أولاً.
1. الحزم الأساسية
توفّر Ubuntu 24.04 الإصدار PHP 8.3 في مستودعاتها الرسمية، مع الإضافات التي يحتاجها Symfony وDoctrine:
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx mysql-server unzip git \
php8.3-fpm php8.3-cli php8.3-mysql php8.3-intl php8.3-mbstring \
php8.3-xml php8.3-curl php8.3-zip php8.3-opcache
# Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
افتح الجدار الناري لحركة الويب (إضافة إلى SSH):
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
2. مستخدم نظام لكل تطبيق
يعمل كل تطبيق بمستخدمه الخاص، فلا تمنح ثغرة في أحدها الوصول إلى ملفات التطبيقات الأخرى.
sudo adduser --system --group --home /var/www/app --shell /bin/bash app
sudo mkdir -p /var/www/app && sudo chown app:app /var/www/app
3. مجموعة PHP-FPM مخصصة
بدلاً من المجموعة الافتراضية www، أنشئ الملف /etc/php/8.3/fpm/pool.d/app.conf. تعمل المجموعة بالمستخدم app، ولا يستطيع التواصل مع المقبس (socket) الخاص بها سوى Nginx (www-data):
[app]
user = app
group = app
listen = /run/php/app.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
php_admin_value[memory_limit] = 256M
php_admin_value[error_log] = /var/log/php/app-error.log
php_admin_flag[log_errors] = on
لتحديد قيمة pm.max_children، اقسم الذاكرة المخصصة لـ PHP على متوسط ذاكرة العملية الواحدة (يظهر عبر ps -o rss -C php-fpm8.3). على خادم بذاكرة 4 غيغابايت، تترك 20 عملية بحجم 80 ميغابايت مساحة كافية لـ MySQL.
sudo mkdir -p /var/log/php && sudo chown app:app /var/log/php
sudo systemctl restart php8.3-fpm
4. ضبط OPcache لبيئة الإنتاج
في الملف /etc/php/8.3/fpm/conf.d/99-production.ini:
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
realpath_cache_size=4096K
realpath_cache_ttl=600
مع validate_timestamps=0 لا يتحقق PHP من تغيّر الملفات، لذا يجب إعادة تحميل PHP-FPM مع كل عملية نشر. هذا هو ثمن مكسب كبير في الأداء.
5. MySQL: قاعدة بيانات ومستخدم مخصصان
sudo mysql
CREATE DATABASE app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'app'@'localhost' IDENTIFIED BY 'a-long-random-password';
GRANT ALL PRIVILEGES ON app.* TO 'app'@'localhost';
FLUSH PRIVILEGES;
في Ubuntu لا يستمع MySQL افتراضياً إلا على 127.0.0.1: أبقِه كذلك ولا تكشفه أبداً على الإنترنت.
6. المضيف الافتراضي في Nginx
إليك الإعداد الموصى به لـ Symfony في الملف /etc/nginx/sites-available/app. الملف index.php وحده قابل للتنفيذ، وأي ملف .php آخر يُرجع الخطأ 404، مما يعطّل أي سكربت ربما رُفع إلى الخادم.
server {
listen 80;
server_name app.example.com;
root /var/www/app/current/public;
client_max_body_size 20M;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/run/php/app.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
internal;
}
location ~ \.php$ {
return 404;
}
location ~* \.(?:css|js|woff2|svg|png|jpg|webp)$ {
expires 30d;
access_log off;
}
error_log /var/log/nginx/app_error.log;
access_log /var/log/nginx/app_access.log;
}
استخدام $realpath_root (بدلاً من $document_root) ضروري إذا كنت تنشر عبر رابط رمزي current: يحلّ Nginx المسار الحقيقي، فلا يستمر OPcache في تقديم الإصدار القديم بعد التبديل.
sudo ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
7. HTTPS مع Let's Encrypt
بعد توجيه النطاق (DNS) إلى الخادم، يحصل Certbot على الشهادة ويعدّل إعدادات Nginx ويضيف إعادة التوجيه من HTTP إلى HTTPS:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com
sudo certbot renew --dry-run # checks automatic renewal
8. نشر الشيفرة
بنية تعتمد على مجلد releases/ ورابط رمزي current تتيح نشراً ذرّياً (atomic) وتراجعاً فورياً عند الحاجة:
sudo -iu app
cd /var/www/app
RELEASE=releases/$(date +%Y%m%d%H%M%S)
git clone --depth 1 git@github.com:me/app.git "$RELEASE"
cd "$RELEASE"
composer install --no-dev --optimize-autoloader --classmap-authoritative
composer dump-env prod # compiles .env into .env.local.php
php bin/console cache:clear
php bin/console doctrine:migrations:migrate --no-interaction
php bin/console asset-map:compile # if you use AssetMapper
cd /var/www/app && ln -sfn "$RELEASE" current
exit
sudo systemctl reload php8.3-fpm
توضع الأسرار (DATABASE_URL وAPP_SECRET) في ملف .env.local مشترك بين الإصدارات، أو الأفضل في خزنة الأسرار الخاصة بـ Symfony (secrets:set). وتؤتمت أدوات مثل Deployer هذه الدورة بالضبط.
9. عمّال Messenger مع systemd
يجب أن يُعاد تشغيل عامل Messenger إذا توقف، وأن يُعاد تشغيله بانتظام لتحرير الذاكرة. أنشئ الملف /etc/systemd/system/app-messenger@.service:
[Unit]
Description=Symfony Messenger worker %i
After=network.target mysql.service
[Service]
User=app
WorkingDirectory=/var/www/app/current
ExecStart=/usr/bin/php bin/console messenger:consume async --time-limit=3600 --memory-limit=256M
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now app-messenger@1 app-messenger@2
أضف php bin/console messenger:stop-workers في نهاية عملية النشر: ينهي العمّال الرسالة الجارية، ثم يعيد systemd تشغيلهم على الشيفرة الجديدة.
10. المهام المجدولة
مع Symfony Scheduler يكفي عامل واحد (messenger:consume scheduler_default). وإلا فاستخدم crontab الخاص بالمستخدم app (sudo crontab -u app -e):
*/5 * * * * cd /var/www/app/current && php bin/console app:sync-data --no-interaction >> /var/log/php/cron.log 2>&1
قائمة التحقق النهائية
APP_ENV=prodوAPP_DEBUG=0: لا profiler أبداً في بيئة الإنتاج- جدار ناري مفعّل، والمنافذ المفتوحة 22 و80 و443 فقط
- MySQL وRedis لا يستمعان إلا على localhost
- تجديد تلقائي للشهادة (
systemctl list-timers | grep certbot) - تدوير السجلات (يتكفّل
logrotateبسجلات Nginx، وأضف/var/log/php/*.log) - نسخ احتياطية تلقائية ومختبَرة لقاعدة البيانات والملفات المرفوعة
- مراقبة الأخطاء (Sentry) ومراقبة توفر الخدمة
يتحمّل هذا الإعداد بسهولة عدة آلاف من المستخدمين يومياً على خادم VPS لا تتجاوز كلفته بضعة يوروهات شهرياً. ولأن كل مكوّن فيه معياري، يسهل تشخيصه عندما يحدث خلل.