Среда разработки для CMS в Docker

19.03.2022 / devops
Развернем среду разработки в Docker для произвольной CMS на php с использованием Apache2, PHP-FPM, MySQL (5.7/8), phpMyAdmin

По работе потребовалось ставить на конвейер разработку модулей под разные CMS для команды разработчиков. Имея опыт работы с Docker, было решено конфигурировать среду разработки модуля для CMS в Docker.

При наличии достаточного пространства на диске, организованная среда на docker контейнерах имеет ряд преимуществ:

В данной статье развернем среду разработки в Docker для произвольной CMS на php с использованием Apache2, PHP-FPM, MySQL (5.7/8), phpMyAdmin. В конце подведем итог.

Когда-то мы уже пробовали развернуть локальный LAMP, теперь развернем его в Docker.

Файловая структура:

Каждый компонент среды будет помещен в отдельный контейнер - микросервисная архитектура.

При разработке плагина для CMS, директорию с исходниками CMS нужно добавить в .gitignore чтобы не коммитить в удаленный репозиторий.

Конфигурации контейнеров

Сначала сконфигурируем каждый сервис/контейнер по отдельности, а потом запишем это в docker-compose.yml.

Возможно, проще для понимания будет параллельно поглядывать на docker-compose.yml.

PHP-FPM

Нужна отладка и тесты на phpunit, значит нужен xdebug и composer.

Создаем файл .docker/php/Dockerfile и записываем в него конфигурацию образа на основе официального образа docker php:

FROM php:7.3-fpm

RUN apt update && apt install -y libzip-dev zip mc nano

# установка модулей php из репозитория ОС
RUN docker-php-ext-install pdo_mysql zip

# установка модулей php из репозитория pecl
RUN pecl install xdebug && docker-php-ext-enable xdebug

# установка composer
RUN cd ~ \
    && curl -sS https://getcomposer.org/installer -o composer-setup.php \
    && php composer-setup.php --install-dir=/usr/local/bin --filename=composer

# запуск PHP-FPM
CMD ["php-fpm"]

Внутрь каждого контейнера предназначенного для разработки, я устанавливаю mc (консольный файловый менеджер) и nano (консольный текстовый редактор), чтобы работать внутри контейнера с удобным для меня ПО.

Внутрь контейнера с PHP будем прокидывать следущие файлы:

[PHP]
user_ini.filename = "php.ini"
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
log_errors = On
error_log = /var/www/html/php_errors.log
xdebug.remote_handler = dbgp
xdebug.log=/var/www/html/xdebug.log
xdebug.client_host = 172.18.0.1
xdebug.client_port = 9003
xdebug.mode=coverage,debug
xdebug.start_with_request = yes
#!/bin/bash

# устанавливаем нужные библиотеки, если надо
composer install

# запускаем вечный цикл, чтобы контейнер не упал
while true; do sleep 10; done;

.docker/php/entrypoint.sh будет запускаться каждый раз при старте контейнера!

APACHE

За основу возьмем официальный Docker образ Apache2 от Canonical (Ubuntu) и внутри образа установим модуль для работы веб-сервера по FastCGI. Запишем все это в конфиг образа .docker/webserver/Dockerfile:

FROM ubuntu/apache2:latest

RUN apt update && apt install -y libapache2-mod-fcgid nano mc

Запускать веб-сервер будем в отдельном скрипте .docker/webserver/entrypoint.sh, имхо удобнее, там несколько команд на запуск:

#!/bin/bash

# включаем нужные модули
a2enmod proxy
a2enmod proxy_fcgi
a2enmod rewrite

# тестируем конфиги
apachectl configtest

# запускаем демона apache2
service apache2 start

# стартуем вечный цикл
while true; do sleep 10; done;

Сделаем конфиг .docker/webserver/host.conf для нашего виртуального хоста:

<VirtualHost *:80>
    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html

    <FilesMatch \.php$>
        # по дефолту PHP-FPM работает на 9000 порту
        # имя хоста php берем из имени сервиса,
        # которое присвоили контейнеру с php в docker-compose-dev.yml
        SetHandler "proxy:fcgi://php:9000"
    </FilesMatch>

    ErrorLog /var/www/html/error.log
    CustomLog /var/www/html/access.log combined
</VirtualHost>

Теперь нам необходим сам конфиг .docker/webserver/apache2.conf. Оригинальный конфиг apache2 в контейнере можно просмотреть так:

# запускаем контейнер в фоне
$ docker run -d ubuntu/apache2

# смотрим список запущенных контейнеров
$ docker ps
CONTAINER ID   IMAGE            COMMAND                CREATED          STATUS          PORTS     NAMES
2464a3b528d2   ubuntu/apache2   "apache2-foreground"   25 seconds ago   Up 23 seconds   80/tcp    heuristic_lederberg

# заходим в терминал в контейнере
$ docker exec -it 2464a3b528d2 /bin/bash

# просматрвиаем файл конфига
$ cat /etc/apache2/apache2.conf
# здесь будет вывод файла конфига

# закрываем сессию терминала в контейнере (выходим из контейнера)
$ exit

# останавливаем контейнер
$ docker stop 2464a3b528d2

Копируем то что в файле конфига, и заменяем некоторые настройки на это:

# ip адрес сервера
ServerName 172.18.0.1

# разрешение на децентрализованную конфигурацию (.htaccess) в диреткории /var/www/
<Directory /var/www/>
	Options Indexes FollowSymLinks
	AllowOverride All
	Require all granted
</Directory>

# название файла с децентрализованным конфигом
AccessFileName .htaccess

IP адрес директивы ServerName должен совпадать с IP адресом подсети создаваемой для композиции контейнеров в docket-compose.yml.

MYSQL

В простом варианте достаточно взять Docker образ MySQL и заюзать переменные из раздела Environment Variables типа MYSQL_ROOT_PASSWORD, MYSQL_USER и MYSQL_PASSWORD.

Однако, у меня это не заработало в момент когда появилась вторая композиция контейнеров, то есть была одна запущенная композиция контейнеров с MySQL и все работало, но при попытке поднять вторую композицию контейнеров с использованием MySQL начались проблемы, что-то вроде такого:

Lost connection to MySQL server at 'reading initial communication packet', system error: 104
# или
Can't open and lock privilege tables: Table 'mysql.user' doesn't exist
# или
Can't open the mysql.plugin table. Please run mysql_upgrade to create it

И да, я не пытался поднять второй инстанс одной и той же композиции контейнеров, нет.

Все сводилось к тому, что сервис MySQL не мог инициализироваться.

В одном случае могла успешно работать композиция контейнеров 1, но не работала 2-ая композиция, но после docker system prune -a, магичеким образом переставала работаться 1-ая композиция, а 2-ая работала.

Решение: провести инициализацию самостоятельно при первом запуске сервиса.

Для этого нужно 2 файла.

.docker/mysql/entrypoint.sh:

#!/bin/bash

# инициализировать mysql без root пароля
# в первый запуск сработает, во второй нет, потому что хранилище уже инициализировано
mysqld --initialize-insecure

# запустить демона mysql в фоне
mysqld --user=root --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --default-authentication-plugin=mysql_native_password --daemonize

# зайти в mysql от root но без пароля и передать на выполнение содержимое файла
# в первый запуск сработает, во второй не сможет зайти без пароля
mysql -u root < /usr/bin/init.sql

# запустить вечный цикл
while true; do sleep 10; done;

.docker/mysql/init.sql для MySQL 5.7:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';
GRANT ALL ON *.* to 'root'@'%' IDENTIFIED BY 'root';
FLUSH PRIVILEGES;

CREATE DATABASE IF NOT EXISTS db_name;
USE db_name;

.docker/mysql/init.sql для MySQL 8.0:

CREATE USER 'root'@'%' IDENTIFIED WITH mysql_native_password  BY 'root';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%';
FLUSH PRIVILEGES;

CREATE DATABASE IF NOT EXISTS db_name;
USE db_name;

'root'@'%' позволяет указать что root пользователь может заходить с любого хоста. localhost будет доступен только в контейнере с MySQL, но к сервису MySQL в композиции контейнеров, будут обращаться другие контейнеры не по localhost, а по имени сервиса MySQL, например 'root'@'mysql'.

phpMyAdmin

А здесь ничего дополнительно конфигурировать не будем, просто возьмем Docker образ phpMyAdmin, прокинем порт контейнера на хост машину, и укажем доступы к сервису mysql прямо в docker-compose.yml, и все будет работать :)

docker-compose

Сделаем docker-compose.yml (документация):

version: '3.0'

services:

  mysql:
    image: mysql:5.7
    container_name: cms-mysql-dev
    restart: unless-stopped
    volumes:
      # хранилище mysql прокинем на хост машину
      - ./docker-data/mysql/data:/var/lib/mysql
      - ./.docker/mysql/init.sql:/usr/bin/init.sql
      - ./.docker/mysql/entrypoint.sh:/usr/bin/entrypoint.sh
    command: /bin/bash -c "/usr/bin/entrypoint.sh"
    networks:
      - app-network
    
  php:
    build:
      context: ./.docker/php
    container_name: cms-php-dev
    restart: unless-stopped
    depends_on: 
      - mysql
    volumes:
      # монтируем файлы cms (и плагина) внутрь контейнера
    networks:
      - app-network
      
  webserver:
    build:
      context: ./.docker/webserver
    container_name: cms-webserver-dev
    restart: unless-stopped
    depends_on: 
      - php
    ports:
      - "5011:80"
    volumes:
      - ./.docker/webserver/host.conf:/etc/apache2/sites-available/000-default.conf
      - ./.docker/webserver/apache2.conf:/etc/apache2/apache2.conf
      - ./.docker/webserver/entrypoint.sh:/usr/bin/entrypoint.sh
    command: /bin/bash -c "/usr/bin/entrypoint.sh"
    networks:
      - app-network
      
  pma:
    image: phpmyadmin/phpmyadmin:latest
    container_name: cms-pma-dev
    restart: unless-stopped
    depends_on: 
      - mysql
    ports:
      - "5012:80"
    networks:
      - app-network

networks:
  app-network:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.18.0.1/24

В терминал введем команду:

$ docker-compose up --build

И наш пустой web-сервер заработает, можно проверить по адресу localhost:5011.

Настало время разобраться как же сюда впихнуть CMS ...

CMS в Docker

Чтобы CMS заработала в Docker ее надо прокинуть/смонтировать - снаружи внутрь, и тогда можно будет редактировать файлы на хост машине, а изменения будут вступать в силу сразу при сохранении файлов на диск.

В случае когда работа осуществляется с целым инстансом CMS и этот ##инстанс CMS является продуктом##, то на этом развертывание среды заканчивается - монтируем CMS, запукаем docker-compose, заходим на localhost:5011 и производим установку CMS.

Если в качестве продукта выступает ##плагин##/##модуль##/##дополнение## для CMS, тогда, кроме прокидывания CMS нужно настроить прокидывание файлов модуля - сначала монтируются файлы CMS, затем файлы модуля.

Если говорить про плагины для Wordpress, 1С-Битрикс, Shop-Script и прочие CMS где плагин располагается в отдельной директории, то достаточно прокинуть только эту директорию.

Например, в корне есть такие директории:

Тогда монтирование CMS и исходников будет выглядеть так:

volumes:
  - ./shop-script/:/var/www/html/
  - ./src/:/var/www/html/wa-apps/shop/plugins/mymodule/

А если файлы плагина внедряются смешиваясь с основными файлами CMS, как например сделано в OpenCart, то прокидывать файлы плагина надо по отдельности каждый.

Например, в корне есть такие диреткории:

При попытке такого монтирования:

volumes:
  - ./opencart/:/var/www/html/
  - ./src/:/var/www/html/

Получим такую ошибку:

ERROR: Duplicate mount points: [/home/byurrer/modules/opencart3.0/opencart:/var/www/html:rw, /home/byurrer/modules/opencart3.0/src:/var/www/html:rw]

А вот так, многословно но работает:

volumes:
  - ./opencart/:/var/www/html/
  - ./src/admin/controller/extension/module/mymodule.php:/var/www/html/admin/controller/extension/module/mymodule.php
  - ./src/catalog/controller/extension/module/mymodule.php:/var/www/html/catalog/controller/extension/module/mymodule.php
  - ./src/admin/language/ru-ru/extension/module/mymodule.php:/var/www/html/admin/language/ru-ru/extension/module/mymodule.php

Итог

В итоге мы получаем среду для работы с CMS в Docker контейнерах, организованных при помощи композиции контейнеров. При этом работа с CMS может осуществляться в двух вариантах:

По адресу localhost:5011 будет доступен сайт на CMS, а по адресу localhost:5012 будет доступен phpMyAdmin.