مثال واقعي على استخدام حاويات Docker لتشغيل موقع ويب

عمار الخوالدةمنذ 5 سنوات

مقدمة

في هذا الدرس سنقوم بتطبيق ما تعلمناه مسبقا لتوفير بيئة عمل لتطبيق PHP باستخدام إطار العمل Laravel.

كما تعلمنا سابقا، فان من الممارسات الجيدة فصل الحاويات بحيث تقدم كل حاوية خدمة واحدة، ففي هذا المثال سننشئ ثلاث حاويات، حاوية لسيرفر Nginx، وحاوية لمفسر PHP، وأخرى لـ MySql. بعد هذا المثال سيتضح لك بشكل أفضل فائدة فصل الخدمات بعضها عن بعض، فلو احتجت الآن إلى Redis في المشروع فكل ما عليك اضافة الصورة الخاصة به وكتابة بضعة أسطر في ملف Docker Compose لتربطه ببقية الحاويات، كذلك لو احتجت لتغيير نوع قاعدة البيانات، فكل ما عليك فعله تغيير الصورة التي تستدعيها لقاعدة البيانات وهكذا.

إضافة إلى ذلك سنقوم بكتابة سكربت بسيط ليقوم بإنشاء مشروع Laravel بشكل تلقائي في حال عدم توفر مشروع.

 

ملفات المشروع

 

هذه هي الملفات التي سنحتاجها لانشاء المشروع وسنوضح محتويات الملفات قبل البدء بشرحها:

  • env. : هو ملف يحتوي على إعدادات Laravel وإعدادات الحاويات التي سنقوم بانشائها.
  • docker-compose.yml: ملف Docker Compose الذي سيقوم بإدارة وانشاء الحاويات.
  • nginx.conf: ملف إعدادات سيرفر Nginx، لتفاصيل أكثر راجع هذه الدروس: (1, 2).
  • php-entry.sh: سكربت bash يقوم بانشاء مشروع Laravel جديد في حال عدم وجود مشروع.
  • php.Dockerfile: الملف الذي سنقوم من خلاله ببناء صورة PHP.

إضافة إلى مجلد www الذي سيحتوي على ملفات المشروع.

 

إنشاء ملف الإعدادات

تستخدم لارافيل ملف env. لتحديد إعداداتها، وبما أن Docker Compose يدعم هذا النوع من الملفات فسنقوم باستغلال هذا الملف لتحديد بعض الإعدادات التي سنستخدمها لبناء الحاويات، وبعد الانتهاء من بناء الحاويات سنقوم بنسخ ملف env. إلى مجلد لارافيل لاستخدامه في المشروع وهذه صورة للملف مع توضيح المتغيرات التي سنقوم باستخدامها في ملفات Docker و Docker Compose:

 

 

باقي المتغيرات هي متغيرات تُستخدم في لارافيل ولا حاجة لنا بها في إنشاء الحاويات.

 

إنشاء الحاويات

هكذا ستكون بداية ملف docker-compose.yml:

version: '3'

services: 

# هنا سنقوم بإنشاء الخدمات

 

بما أن Nginx سيحتاج للاتصال بمفسر PHP ليمرر له ملفات PHP التي يقوم بتشغيلها، فإن أول حاوية يجب تشغيلها هي حاوية Nginx، و PHP ستحتاج للاتصال بقاعدة البيانات لتقوم باستخدامها إذا تطلب المشروع ذلك، وقد تعلمنا أننا نحدد ترتيب انشاء الحاويات باستخدام الأمر depends_on، لكن أثناء الشرح، سنشرح أولا الخدمات ذات التفاصيل الأقل، لذلك سنبدأ بشرح انشاء خدمة Mysql ثم خدمة PHP وأخيرا خدمة Nginx، لكن Docker سيقوم بتشغيل الحاويات بالترتيب الصحيح بسبب استخدام depends_on.

 

إنشاء خدمة Mysql

  database:
    # database إسم الخدمة الذي سنتعامل معها من خلاله هو

    image: mysql:5.7
    # Mysql صورة 

    environment:
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_USER: ${DB_USERNAME} 
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}

 

كما ترى فقد قمنا بتحديد الصورة التي سيتم تحميلها وإنشاء حاوية منها، ولاحظ أننا قمنا بإدراج أربعة متغيرات، وهذه المتغيرات هي المسؤولة عن إعدادات Mysql، فالمتغير MYSQL_DATABASE متغيرة يقوم بإنشاء قاعدة بيانات جديدة - إن لم تكن موجودة مسبقا - بنفس الإسم الذي يُزود به، وكما تُلاحظُ فإننا قمنا بتزويده باسم قاعدة البيانات المحددة في المتغير DB_DATABSE في الملف env.، أما المتغير MYSQL_USER فيقوم بإنشاء مستخدم جديد وإعطائه الصلاحيات اللازمة، و MYSQL_PASSWORD تقوم بتحديد كلمة السر لهذا المستخدم، وتختلف عنها MYSQL_ROOT_PASSWORD بأنها تقوم بتحديد كلمة السر للمستخدم root.

يمكنك قراءة المزيد عن هذه المتغيرات وغيرها من التوثيق الرسمي لـ Mysql أو من التوثيق الرسمي لصورة Mysql على Docker Hub.

 

لا تزال هناك مشكلتان في الخدمة السابقة، فمن غير الممكن الاتصال بالخدمة دون وجود Port مفتوح يمكن حاوية PHP من الاتصال بحاوية Mysql، كذلك فإن البيانات المخزنة في قاعدة البيانات ستُحذف بمجرد إغلاق الحاوية.

لحل المشكلة الأولى نقوم بتحديد البورت الذي تستخدمه Mysql في الخدمة ليُصبح بالإمكان استخدام هذا البورت للاتصال بقاعدة البيانات، والبورت الافتراضي هو البورت 3306 (تفاصيل).

أما لحل المشكلة الثانية فإننا نقوم بإنشاء Volume بين الجهاز المستضيف، وملفات Mysql داخل الحاوية، وإن لم تُرِد أن تحدد مسارا محددا للملفات فقم بإنشاء Volume جديد وستوضع الملفات ضمن مجلد Docker في جهازك.

 

وبتنفيذ الحلول السابقة يصبح الملف بهذا الشكل:

version: '3'

services:
  database:
    image: mysql:5.7
    ports:
      - 3306:3306
    volumes:
      - db:/var/lib/mysql
    environment:
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_USER: ${DB_USERNAME} 
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}

volumes:
  db:
    

 

إنشاء خدمة PHP

 

قبل إنشاء خدمة PHP في ملف docker-compose.yml يجب أن نُجهِّزَ الصورة، فقمنا كما رأيت في ترتيب الملفات بإنشاء ملف php.Dockerfile، في هذا المثال سأستخدم نسخة php 7.1، وسأبدأ الملف بالأمر FROM لتحديد الصورة التي سنبني عليها صورتنا:

FROM php:7.1-fpm

 

من ثم سنقوم بتنصيب الحزم اللازمة بتشغيل مشروع Laravel من مدير الحزم:

RUN apt-get update && apt-get install -y zlib1g-dev \
    libmcrypt-dev && docker-php-ext-install pdo_mysql mcrypt zip 

 

لاحظ استخدام الأمر docker-php-ext-install وهذا الأمر توفره صورة PHP الرسمية على Docker Hub لتسهيل تنصيب إضافات PHP، يمكنك معرفة المزيد من التفاصيل من الصفحة الرسمية لصورة PHP على Docker Hub ( من هنا ).

حتى نتمكن من تنصيب وإعداد مشروع Laravel والتعامل مع حزم PHP، فإننا بحاجة لتنصيب Composer ( مدير حزم PHP ):

 

RUN php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer

ENV COMPOSER_ALLOW_SUPERUSER 1

الأمر الأول هو أمر عادي يقوم بجلب مُنصب Composer من الموقع الرسمي وتمريره لـ PHP لتقوم بتنصيبه ( فهو عبارة عن سكربت مكتوب بلغة PHP )، أما الأمر الثاني فهو يقوم بإنشاء متغير بيئة في الحاوية، وهذا المتغير يسمح لنا باستخدام Composer من حساب يمتلك صلاحيات Root، فبشكل افتراضي لا يمكنك فعل ذلك إلا بتفعيل هذا المتغير ( تفاصيل ).

حتى الآن أصبح لدينا صورة PHP تستطيع تشغيل مشاريع لارافيل وادارة الحزم باستخدام Composer، لكن تبقى لدينا مشكلة في إدارة الصلاحيات، فلكل مستخدم في Linux رقم يميزه يُسمى UID، و PHP يمكنها تعديل ملف معين على السيرفر باستخدام User باسم www-data، لذلك يجب أن يكون المالك (Owner) لملفات المشروع هو المستخدم www-data أو المجموعة www-data، وغالبا في صورة PHP سيكون الـ UID الخاص بالمستخدم www-data هو 33، لكن الـ UID الخاص بجهازك سيكون مختلفا بالغالب ( يمكنك معرفته بتنفيذ هذا الأمر: id -u )، لذلك ستواجه مشكلة في الصلاحيات، فإذا كان المالك لملفات المشروع هو www-data فلن تتمكن من تعديل الملفات من جهازك الشخصي، أما إن كان مالك المشروع هو المستخدم الخاص بجهازك ( ليكن 1000 مثلا ) فلن يتمكن www-data من الوصول إلى الملفات.

فما الحل في هذه الحالة ؟
في الحقيقة يوجد أكثر من حل، كأن تقوم بإنشاء Group على جهازك والـ GID الخاص به يساوي الـ UID الخاص بـ www-data على الحاوية، وتوجد حلول أخرى أيضا، لكن الحل الأسهل من وجهة نظري هو تغيير الـ UID الخاص بالمستخدم www-data في الحاوية، وجعل الـ UID الجديد يساوي الـ UID الخاص بمستخدم جهازك الشخصي، كذلك بالنسبة للـ GID الخاص بالمجموعة www-data لذلك نضيف هذه الأوامر إلى ملف الصورة:

ARG USER_ID

RUN usermod -u ${USER_ID} www-data && groupmod -g ${USER_ID} www-data

 

لاحظ أننا نطلب منه ARG باسم USER_ID، أي أننا سنمرره له أثناء بناء الصورة ( من داخل ملف docker-compose.yml )، ثم نستخدمه بداخل الأمر usermod و groupmod.

 

وأخيرا ننهي هذا الملف بأمرين بسيطين:

WORKDIR /var/www
 
RUN chown -R www-data:www-data ./

نحدد الـ WORKDIR في الأمر الأول، والأمر الثاني سيقوم بتغيير مالك الملفات الموجودة داخل /var/www ( والذي سنقوم بعمل Volume له بعد قليل) ليجعل مالكها هو www-data وبالطبع يمكنك تعديل الملفات من جهازك الشخصي فالـ UID نفسه.

 

بهذا نكون قد أنهينا كتابة الأوامر التي ستُبنى الصورة على أساسها، وسنقوم الآن بكتابة الـ Service في ملف docker-compose.yml، أولا نحدد الصورة التي سيتم بناؤها:

  php:
    build:
      context: ./
      dockerfile: php.Dockerfile


لاحظ أن الأمر build هنا قد أخذ خاصيتين وليس خاصية واحدة كما تعلمنا في الدرس السابق، فسابقا كنا نحدد المجلد الذي يحتوي على ملف Dockerfile، أما هنا فبما أننا قمنا بتغيير اسم الملف إلى (php.Dockerfile) فلن يتم التعرف عليه، ففي الخاصية context نحدد المجلد، أما في الخاصية dockerfile نقوم بتحديد اسم الملف.

 

وكما رأينا سابقا، فإننا بحاجة إلى تمرير ARG باسم USER_ID إلى الصورة:

  php:
    build:
      context: ./
      dockerfile: php.Dockerfile
      args: 
        USER_ID: ${USER_ID}


لاحظ أننا نمرر لـ USER_ID متغيرا باسم USER_ID أيضا، وستُجلب قيمته من ملف env.، لذلك فعند إعداد ملف env. عليك وضع الـ UID الخاص بالمستخدم في جهازك كقيمة للمتغير.

باقي الأوامر أوامر عادية سنشرحها بشكل عابر:

  php:
    build:
      context: ./
      dockerfile: php.Dockerfile
      args: 
        USER_ID: ${USER_ID}
    volumes:
      - ./www:/var/www
      - ./php-entry.sh:/php-entry.sh
      - ./.env:/.env
    environment: 
      APP_NAME: ${APP_NAME}
    ports:
      - 9000:9000
    depends_on:
      - database
    working_dir:
      /var/www
    user: '1000'
    entrypoint: '/php-entry.sh'


قمنا بتعريف 3 Volumes، الأول لملفات المشروع ( كما قلنا في بداية الشرح أن المجلد www سيحتوي ملفات المشروع ) ويربط ملفات المشروع بالمسار var/www/ في الحاوية، والثاني ينقل ملف php-entry.sh إلى الحاوية ( لنقوم باستخدامه كـ entrypoint ) أما الثالث فسينقل ملف env. إلى الحاوية لنقوم باستخدامه فيما بعد كملف إعدادات لمشروع Laravel.

 

من ثم قمنا بإدخال متغير البيئة APP_NAME والذي يحتوي اسم المشروع ( سنستخدم هذا المتغير في الـ Entrypont )، إضافة إلى تحديد البورت الذي تستخدمه php ( لتمكين Nginx من التواصل مع PHP )، وحددنا أن الحاوية ستعتمد على حاوية قاعدتة البيانات.

أما user فيُستخدم لتحديد المستخدم الافتراضي الذي سيتم إدخالنا إليه في حال استخدام الأمر exec.

وفي النهاية حددنا السكربت الذي سيستخدم كـ Entrypoint للحاوية، وهذا هو سكربت الحاوية مشروحا بالتعليقات داخل السكربت ( لاحظ استخدام المتغير APP_NAME الذي عرفناه مسبقا للتعامل مع مجلد المشروع المناسب ).

#! /bin/bash
if [ ! -f /var/www/${APP_NAME}/public/index.php ]; then  # هذا الشرط يتحقق من وجود مشروع أم لا
# وسينفذ الشرط في حالة عدم وجود مشروع
  composer create-project --prefer-dist laravel/laravel ${APP_NAME};
  # في حال عدم وجود مشروع سيقوم أولا بانشاء مشروع جديد
  
  rm ./${APP_NAME}/.env;
  cp /.env ./${APP_NAME}/.env;
  # ثم نحذف ملف الإعدادات الافتراضي وننقل ملف الاعدادات الخاص بنا

  cd ${APP_NAME};
  php artisan key:generate;
  # وهذا أمر خاص بلارافيل لتكوين مفتاح جديد
fi

docker-php-entrypoint php-fpm;
# الافتراضية الخاصة بالصورة entrypoint هذه هي الـ 
# ونقوم بتنفيذها لتعمل الصورة كما يجب بعد انتهاء الشرط الخاص بنا



إنشاء خدمة Nginx

 

بداية فإننا بحاجة لجلب صورة Nginx وجعل الحاوية تعتمد على حاوية PHP ليتم تشغيل الحاويات بالترتيب المناسب وإعداد البورت، وتذكر أننا قمنا بإنشاء متغير في ملف env. لتحديد البورت الذي سنقوم باستخدامه لـ Nginx ( ملاحظة: في كل الأحوال سنستخدم البورت 80 في إعدادات Nginx، لكننا سنقوم بربطه بالبورت المحدد في ملف env. لنتمكن من الوصل إليه من جهازنا الشخصي):

  nginx:
    image: nginx:1.10.3
    ports:
      - ${NGINX_PORT}:80
    depends_on:
      - php
    working_dir:
      /var/www

 

بالطبع فإن Nginx سيكون بحاجة للوصول إلى ملفات المشروع، لذلك سننشئ Volume لملفات المشروع، وآخر لملف إعدادات Nginx لكن قبل ذلك دعنا نكتب محتويات ملف الإعدادات ودقق على الملاحظات التي سنكتبها ضمن التعليقات:


server {
    listen 80;
    server_name localhost;
    root /var/www/${project_name}/public;
    # في ملف المشروع public موجود في المجلد index.php ملف 
    # Composer ولاحظ أننا حددنا ملف المشروع الذي سيتم إنشاؤه بواسطة 
    # في الحاوية Environment Variables عن طريق استخدام متغير سنعرفه ضمن

    index index.html index.htm index.php;

    charset utf-8;

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

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass php:9000;
        # بمسار nginx بدلا من تزويد php لاحظ أننا مررنا اسم الخدمة وهو 
        # php وقمنا باستخدام البورت 9000 الذي حددناه في خدمة
        
        fastcgi_index index.php;
        include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    }

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

 

كما تلاحظ أعلاه فإننا بحاجة إلى استخدام متغير بيئة داخل ملف nginx.conf، لكن المشكلة أن ملفات إعدادات Nginx لا تدعم متغيرات البيئة، لذلك فالحل في استخدام أداة envsubst والتي تقوم باستبدال المتغيرات الموجودة في أي ملف نصي بقيمها في متغيرات البيئة، وقد تم شرح ذلك في التوثيق الرسمي لصورة Nginx في Docker Hub، وسنستخدم هذه الأداة كأمر (command) في الخدمة بحيث يقوم بمعالجة المتغير ثم نقل ملف إعدادات nginx إلى مكانه المناسب وأخيرا يشغل خدمة Nginx:

 

  nginx:
    image: nginx:1.10.3
    ports:
      - ${NGINX_PORT}:80
    depends_on:
      - php
    working_dir:
      /var/www
    volumes:
      - ./www:/var/www
      - ./nginx.conf:/default.conf
    environment: 
      project_name: ${APP_NAME}
    command: /bin/bash -c "envsubst '$$project_name' < /default.conf > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"


لاحظ أننا في الـ Volume جعلنا الملف يُربط بمسار آخر غير مسار إعدادات Nginx، وذلك لأننا سننقله لاحقا إلى المسار المناسب بعد تبديل قيم المتغيرات باستخدام envsubst، وقمنا كذلك بإضافة متغير البيئة الذي سنستخدمه، ثم نفذنا الأمر الذي يعالج المتغير باستخدام envsubst ثم يَنقُلُ الناتج إلى ملف إعدادات nginx، وبعدها يشغل خدمة Nginx.

 

والآن قم بتشغيل الخدمة باستخدام docker-compose up وانتظر قيام Composer بانشاء المشروع، ثم شغل localhost:8000 وسيعمل الموقع:

 

 

للحصول على الأوامر والملفات السابقة كاملة راجع هذا المستودع:

https://github.com/3mmarg97/Laravel-LEMP-Docker-Compose

 

المحاضر

عمار الخوالدة

عن الدرس

5 إعجاب
1 متابع
0 مشاركة
4485 مشاهدات
منذ 5 سنوات

التعليقات (0)

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !