البرمجة متقدّم 16 دقيقة

كيفية وضع تطبيق Laravel في Docker مع PHP-FPM و Nginx

إعداد صورة واحدة تضم "PHP + Nginx + MySQL في حاوية واحدة" مغرٍ لكنه خاطئ — إنه يتعارض مع نموذج عمليات Docker، ويجعل التوسع مستحيلاً، ويضخّم الصورة. النهج الصحيح هو أربع حاويات: php-fpm وnginx وmysql وredis، مربوطة معاً بـ Compose. هذا الدليل يبني تلك البيئة من الصفر بـ Dockerfile متعدد المراحل ينتج صورة إنتاج خفيفة.

الخطوات

  1. 1

    اكتب Dockerfile متعدد المراحل

    المرحلة الأولى (builder) تثبّت تبعيات Composer. المرحلة الثانية (runtime) تنسخ فقط مجلد vendor — لا Composer نفسه. الصورة النهائية تبقى صغيرة وخالية من أدوات البناء.

    dockerfile
    # ── Stage 1: Composer dependencies ──────────────────────────────
    FROM composer:2 AS builder
    WORKDIR /app
    COPY composer.json composer.lock ./
    RUN composer install \
          --no-dev \
          --no-scripts \
          --optimize-autoloader \
          --no-interaction \
          --prefer-dist
    
    COPY . .
    RUN composer run-script post-autoload-dump
    
    # ── Stage 2: PHP-FPM runtime ──────────────────────────────────
    FROM php:8.3-fpm-alpine AS runtime
    
    # تثبيت تبعيات النظام وامتدادات PHP
    RUN apk add --no-cache \
          libpng-dev libjpeg-turbo-dev libwebp-dev \
          libzip-dev oniguruma-dev icu-dev && \
        docker-php-ext-install \
          pdo_mysql mbstring zip bcmath intl gd opcache
    
    WORKDIR /var/www
    
    # نسخ التطبيق وvendor من builder
    COPY --from=builder /app .
    
    # مستخدم بدون صلاحيات root
    RUN addgroup -g 1000 laravel && \
        adduser -u 1000 -G laravel -s /bin/sh -D laravel && \
        chown -R laravel:laravel /var/www
    
    USER laravel
    
    EXPOSE 9000
    CMD ["php-fpm"]
  2. 2

    أنشئ إعداد Nginx لـ PHP-FPM

    يُحيل Nginx طلبات PHP إلى php:9000 — وهذا اسم الخدمة في Compose، لا منفذ محلي. الملفات الثابتة تُقدَّم مباشرة دون المرور بـ PHP.

    nginx
    # docker/nginx/default.conf
    server {
        listen 80;
        root /var/www/public;
        index index.php;
    
        # تقديم الملفات الثابتة مباشرة
        location / {
            try_files $uri $uri/ /index.php?$query_string;
        }
    
        # إحالة طلبات PHP إلى php-fpm
        location ~ \.php$ {
            fastcgi_pass   php:9000;
            fastcgi_index  index.php;
            include        fastcgi_params;
            fastcgi_param  SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
            fastcgi_param  DOCUMENT_ROOT   $realpath_root;
        }
    
        location ~ /\.ht {
            deny all;
        }
    }
  3. 3

    اكتب ملف docker-compose.yml

    أربع خدمات. كلها على شبكة داخلية واحدة laravel. المجلدات تُبقي بيانات قاعدة البيانات ومجلد storage محفوظة عبر إعادة تشغيل الحاويات.

    yaml
    services:
      php:
        build:
          context: .
          target: runtime
        volumes:
          - ./storage:/var/www/storage
        env_file: .env
        networks:
          - laravel
    
      nginx:
        image: nginx:1.27-alpine
        ports:
          - "80:80"
        volumes:
          - ./public:/var/www/public:ro
          - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
        depends_on:
          - php
        networks:
          - laravel
    
      mysql:
        image: mysql:8.4
        environment:
          MYSQL_ROOT_PASSWORD: "${DB_PASSWORD}"
          MYSQL_DATABASE: "${DB_DATABASE}"
        volumes:
          - mysql_data:/var/lib/mysql
        networks:
          - laravel
    
      redis:
        image: redis:7.4-alpine
        networks:
          - laravel
    
    volumes:
      mysql_data:
    
    networks:
      laravel:
  4. 4

    أنشئ ملف .dockerignore

    بدون هذا الملف، يتضمن سياق بناء Docker مجلدَي vendor/ وnode_modules/ — مئات الميجابايت تُرسل إلى الـ daemon عند كل بناء، حتى لو لم يتغير شيء.

    bash
    vendor/
    node_modules/
    .git/
    .env
    .env.*
    storage/logs/*
    storage/framework/cache/*
    storage/framework/sessions/*
    storage/framework/views/*
    tests/
    docker/
    *.md
  5. 5

    شغّل الـ migrations عبر سكريبت entrypoint

    يجب تشغيل الـ migrations بعد أن تصبح mysql جاهزة فعلاً، لا مجرد مشغّلة. استخدم entrypoint صغيراً ينتظر قاعدة البيانات قبل تشغيل artisan.

    bash
    #!/bin/sh
    # docker/php/entrypoint.sh
    
    set -e
    
    echo "Waiting for MySQL..."
    until php -r "new PDO('mysql:host=${DB_HOST};dbname=${DB_DATABASE}', '${DB_USERNAME}', '${DB_PASSWORD}');" 2>/dev/null; do
      sleep 1
    done
    
    php artisan migrate --force
    php artisan optimize
    php artisan storage:link --force
    
    exec "$@"
  6. 6

    اربط الـ entrypoint بالـ Dockerfile

    أضف سطرين إلى مرحلة runtime، قبيل CMD النهائي مباشرة. ENTRYPOINT يشغّل سكريبت الـ migration؛ CMD يمرر php-fpm كوسيط إلى "$@".

    dockerfile
    # أضف إلى مرحلة runtime في Dockerfile (كـ root، قبل USER laravel)
    COPY docker/php/entrypoint.sh /usr/local/bin/entrypoint.sh
    RUN chmod +x /usr/local/bin/entrypoint.sh
    
    USER laravel
    ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
    CMD ["php-fpm"]
  7. 7

    عيّن متغيرات البيئة للحاويات

    ملف .env العادي يعمل — فقط غيّر أسماء المضيفين لتطابق أسماء خدمات Compose. MySQL هي mysql، وRedis هو redis، لا 127.0.0.1.

    bash
    APP_ENV=production
    APP_KEY=base64:your_key_here
    APP_DEBUG=false
    APP_URL=https://example.com
    
    DB_CONNECTION=mysql
    DB_HOST=mysql
    DB_PORT=3306
    DB_DATABASE=laravel
    DB_USERNAME=root
    DB_PASSWORD=secret
    
    REDIS_HOST=redis
    REDIS_PORT=6379
    
    CACHE_STORE=redis
    SESSION_DRIVER=redis
    QUEUE_CONNECTION=redis
  8. 8

    ابنِ البيئة وشغّلها

    ابنِ صورة PHP وشغّل جميع الحاويات الأربع. راقب السجلات للتأكد من أن PHP-FPM وNginx يعملان بنظافة.

    bash
    # البناء والتشغيل (في الخلفية)
    docker compose up -d --build
    
    # مراقبة جميع السجلات
    docker compose logs -f
    
    # تأكد من تشغيل جميع الحاويات
    docker compose ps
    
    # تشغيل أوامر artisan داخل الحاوية
    docker compose exec php php artisan tinker
    
    # إيقاف كل شيء (يحتفظ بالمجلدات)
    docker compose down
    
    # إيقاف ومسح قاعدة البيانات (خطر: يحذف البيانات)
    docker compose down -v
  9. 9

    قائمة تدقيق تصليب الإنتاج

    قبل النشر للإنتاج، طبّق هذه التغييرات:

    • مستخدم بدون root — منجز بالفعل في Dockerfile أعلاه (المستخدم laravel)
    • بدون تبعيات تطوير — منجز بـ composer install --no-dev
    • مجلد storage للقراءة فقط — شارك storage/ وحدها، لا التطبيق بأكمله
    • فحوصات الصحة — أضف كتل healthcheck في Compose حتى تنتظر الخدمات التابعة بشكل صحيح
    • إدارة الأسرار — استخدم Docker secrets أو مدير أسرار (Vault أو AWS SSM) بدلاً من ملف .env عادي في الإنتاج
    • فحص الصور — شغّل docker scout cves أو Trivy على صورتك النهائية قبل الرفع
    yaml
    # أضف health check لخدمة mysql في docker-compose.yml
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    
    # فحص الثغرات الأمنية
    docker scout cves local://your-image:latest

نصائح ومحاذير

  • استخدم <code>docker compose watch</code> (Compose v2.22+) للتطوير المحلي — يُزامن تغييرات الملفات داخل الحاوية دون إعادة البناء.
  • ثبّت إصدارات الصور بدقة في الإنتاج (<code>php:8.3.9-fpm-alpine</code> لا <code>php:8.3-fpm-alpine</code>) لمنع التحديثات المفاجئة من كسر بنائك.
  • إعداد PHP <code>opcache.validate_timestamps=0</code> ضروري للإنتاج — يمنع OPcache من إجراء stat() لكل ملف عند كل طلب. عيّنه في <code>docker/php/opcache.ini</code>.
  • لا تدمج الأسرار في الصورة أبداً. وسائط بناء Docker تصبح جزءاً من تاريخ طبقة الصورة — أي شخص لديه صلاحية الوصول للصورة يستطيع قراءتها بـ <code>docker history</code>.
  • شغّل <code>docker compose exec php php artisan queue:work</code> أو أضف خدمة <code>worker</code> مخصصة في Compose للمهام الخلفية.

خاتمة

لديك الآن بيئة Laravel من أربع حاويات تعكس الإنتاج، تعمل بأمر واحد docker compose up، وتنتج صورة خفيفة عبر البناء متعدد المراحل. يمكن تسليم نفس docker-compose.yml لأي مطور في الفريق وسيحصل على بيئة متطابقة في دقائق — لا مزيد من نقاشات "يعمل على جهازي".

#Docker #Laravel #DevOps
العودة إلى جميع الأدلة

هل تحتاج مساعدة في مشروعك؟

احجز استشارة مجانية لمدة 30 دقيقة لمناقشة تحدياتك التقنية واستكشاف الحلول معًا.