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

19.03.2022

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

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

  • разработчикам не нужно самостоятельно настраивать рабочую среду, а это влечет следующее:
    • быстрый вход в разработку
    • на машинах разработчиков нет лишнего, кроме docker-engine и docker-compose
  • из developer среды можно на этапе разработки сделать задел для организации:
    • среды для CI/CD
    • production среды
В данной статье развернем среду разработки в Docker для произвольной CMS на php с использованием Apache2, PHP-FPM, MySQL (5.7/8), phpMyAdmin. В конце подведем итог.

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

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

  • .docker - директория с конфигами для организации среды разработки
    • mysql
    • php
    • webserver (не apache чтобы в дальнейшем можно было заменить на любой другой web сервер)
  • cms - директория с исходниками CMS
  • src - директория с исходниками плагина (если нужно)
Каждый компонент среды будет помещен в отдельный контейнер - микросервисная архитектура.

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

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

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

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

PHP-FPM

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

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

•••
plaintext
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 будем прокидывать следущие файлы:

•••
ini
[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

•••
ini
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

  • .docker/php/entrypoint.sh:
•••
bash
#!/bin/bash # устанавливаем нужные библиотеки, если надо composer install # запускаем вечный цикл, чтобы контейнер не упал while true; do sleep 10; done;

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

APACHE

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

•••
plaintext
FROM ubuntu/apache2:latest RUN apt update && apt install -y libapache2-mod-fcgid nano mc

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

•••
bash
#!/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 для нашего виртуального хоста:

•••
plaintext
<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 в контейнере можно просмотреть так:

•••
bash
# запускаем контейнер в фоне $ 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

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

•••
plaintext
# 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 начались проблемы, что-то вроде такого:

•••
plaintext
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:

•••
bash
#!/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:

•••
sql
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:

•••
sql
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 (документация):

•••
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

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

•••
bash
$ 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 где плагин располагается в отдельной директории, то достаточно прокинуть только эту директорию.

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

  • shop-script - исходники CMS
  • src - исходники плагина
Тогда монтирование CMS и исходников будет выглядеть так:
•••
yml
volumes: - ./shop-script/:/var/www/html/ - ./src/:/var/www/html/wa-apps/shop/plugins/mymodule/

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

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

  • opencart - исходники CMS
  • src - исходники плагина
При попытке такого монтирования:
•••
yml
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]

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

•••
yml
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.