Docker и PHP

В этой статье мы рассмотрим настройку локального окружения Docker для разработки проектов на PHP

Автор . Дата: 05.03.2019

Думаю многие уже слышали про Docker, а некоторые и использовали. Docker уже довольно старая технология по современным меркам, хайп вокруг докера уже стих. Я уже давно использую докер для локальной разработки, и в этой статье хотел бы поделиться с вами моими знаниями.

Почему именно докер для локальной разработки? Все просто, это очень удобно. Представьте, вы попадаете на новый для вас проект, скачиваете репозиторий с кодом, и вам нужно узнать какие версии PHP, СУБД использутся. Затем установить требуемые версии интепретатора, СУБД. Возможно сразу, а может и потом выяснится, что для PHP нужны дополнительные модули, особые настройки и т.п. Или может вообще используемый софт не представлен для вашей операционной системы. Последнюю проблему решают виртуальные машины, но все же это не очень удобно. В случае с докером, вам нужно будет выполнить пару простых команд, и вы получите готовое окружение, где будет весь софт, нужных версий и с нужными модулями. Красота? И я так считаю :-)

В этой статье мы разберем конфигурацию на три контейнера (nginx, php, mysql), и настроим их для работы фреймворка Yii2. В конце статьи будет ссылка на репозиторий, где будет залит базовый проект на Yii2. а так же добавлены все конфигурационные файлы рассмотренные в этой статье.

Docker compose

Docker это контейнерная система, где каждая единица софта запускается в своем собственном контейнере. Есть возможность запускать контейнеры по одному, но это не очень удобно, поэтому мы будем использовать Docker compose. Docker compose позволяет в одну команду запустить сразу несколько контейнеров, установить связи между ними. Настройки для Docker compose хранятся в файле конфигурации docker-compose.yml. Его необходимо создавать в корне вашего проекта. Файл можно назвать и по другому, но тогда необходимо будет передавать его название в команды.

Рассмотрим пример docker-compose.yml который будет использоваться для запуска локального окружения:

version: '3'

volumes:
  data-db:

services:
  nginx:
    image: nginx:latest
    ports:
      - '${HTTP_PORT}:80'
    volumes:
      - ./:/app
      - ./docker/nginx:/etc/nginx/conf.d
      - ./docker/log:/var/log/container/
    depends_on:
      - php
    networks:
      - project

  php:
    build: ./docker/php
    working_dir: /app
    volumes:
      - ./:/app
      - ./docker/log:/var/log/container
    depends_on:
      - db
    networks:
      - project

  db:
    image: percona:5.7
    ports:
      - '${MYSQL_PORT}:3306'
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - ./docker/mysql/conf.d:/etc/percona-server.conf.d
      - ./docker/log:/var/log/container
      - data-db:/var/lib/mysql
    networks:
      - project

networks:
  project:
    driver: bridge

В нем мы определям 3 контейнера, один volume для хранения данных базы, и одну сеть для связывания контейнеров.

Nginx

Мы будем использовать последнюю версию nginx на текущий момент времени. Обычно его версия слабо влияет на приложение, но если вам необходимо, можете указать какую-то конкретную версию. Для nginx мы монтируем корневую папку проекта в папку /app в контейнере (для того, чтобы nginx мог отдавать статические файлы), папку docker/nginx из проекта монтируем в /etc/nginx/conf.d (для загрузки своего конфига виртуального хоста) и папку docker/log монтируем в /var/log/container (nginx будет в эту папку складывать логи, и мы сможем удобно просматривать их на хостовой машине в случае необходимости). В блоке depends_on укажем зависимость от контейнера php, в блоке networks укажем, что контейнер должен находиться в сети project. В блоке ports укажем маппинг порта nginx на хостовую машину. Номер порта на хостовой машине берем из переменной HTTP_PORT, её можно задать переменной окружения в файле .env Привожу пример настройки виртуального хоста для nginx.

docker/nginx/vhost.conf

server {
    listen 80 default;

    root /app/web;
    index index.php index.html;

    access_log /var/log/container/nginx.access.log php_main;
    error_log /var/log/container/nginx.error.log error;

    sendfile off;
    charset utf-8;

    client_max_body_size 32m;

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

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param SERVER_PORT $server_port;
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        fastcgi_read_timeout 300;
    }
}

В нем мы определяем, что корневой папкой будет папка web находящаяся в корне проекта, в случае обработки php файлов обращаться к контейнеру php на порт 9000.

PHP

Образ для PHP мы будем использовать не готовый, а собирать на основе образа php:7.2-fpm. Dockerfile описывающий параметры сборки мы разместим в папке docker/php. Его содержимое:

# Основываемся на контейнере версии 7.2-fpm
FROM php:7.2-fpm

# Устанавливаем необходимые для расширений пакеты 
RUN apt-get update && apt-get install -y zlib1g-dev libicu-dev \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libmcrypt-dev \
        libmagickwand-dev \
        libmagickcore-dev \
        libpng-dev \
        libxslt1-dev \
        zip unzip \
        --no-install-recommends

# Устанавливаем расширения intl, PDO MySQL, bcmath, xsl, zip, mysqli, soap
RUN pecl channel-update pecl.php.net \
    && docker-php-ext-install intl pdo_mysql bcmath xsl zip mysqli soap \
    && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
    && docker-php-ext-install gd \
    && docker-php-ext-install pcntl && docker-php-ext-enable pcntl \
    && pecl install xdebug \
    && docker-php-ext-enable xdebug

# Устанавливаем vim и git
RUN apt-get update && apt-get install -y \
    build-essential \
    vim \
    git-core

# Удаляем ненужные пакеты и чистим образ
RUN apt-get purge -y g++ \
    && apt-get autoremove -y \
    && rm -r /var/lib/apt/lists/* \
    && rm -rf /tmp/*

# удаляем папку с конфигами по умолчанию для php-fpm
RUN rm -rf /usr/local/etc/php-fpm.d
# копируем конфиги из папки с проектов в контейнер
COPY ./pools /usr/local/etc/php-fpm.d
COPY ./php.ini /usr/local/etc/php/
COPY ./php-fpm.conf /usr/local/etc/php-fpm.conf
COPY ./docker-php-entrypoint /usr/local/bin/

# устанавливаем composer в контейнер
RUN curl -sS https://getcomposer.org/installer | php -- --filename=composer --install-dir=/usr/bin

ENTRYPOINT ["docker-php-entrypoint"]
CMD ["php-fpm"]

В коде файла есть комментарии, которые описывают действия, которые будут выполняться при сборке контейнера. В случае необходимости, вы можете добавить установку дополнительных расширений, или другого ПО (например phpunit). При сборке контейнера мы копируем некоторые конфиги из папки docker/php, привожу их содержимое:

php.ini

error_reporting = E_ALL
error_log = /var/log/container/php.error.log
date.timezone = Europe/Moscow
max_execution_time = 600
memory_limit = 1024M

php-fpm.conf

[global]
error_log = /var/log/container/fpm.error.log
log_level = notice
daemonize = no
include=/usr/local/etc/php-fpm.d/*.conf

docker-php-entrypoint

#!/usr/bin/env sh
set -e

CONFD_PATH=$(php --ini | grep 'Scan for additional' | sed -- 's/^.*:\s*\([^ ].*\)$/\1/')

# first arg is `-f` or `--some-option`
if [ "${1#-}" != "$1" ]; then
	set -- php "$@"
fi

exec "$@"

pools/www.conf

[www]
clear_env = no

listen = 0.0.0.0:9000

listen.owner = www-data
listen.group = www-data

listen.mode = 0666

pm = dynamic
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 2
pm.max_children = 2
pm.process_idle_timeout = 10s
pm.max_requests = 200

chdir = /app

user = www-data
group = www-data

request_slowlog_timeout = 10
slowlog = /var/log/container/fpm.slow.log
access.log = /var/log/container/fpm.access.log
php_admin_value[error_log] = /var/log/container/fpm.error.log
catch_workers_output = yes

Контейнер зависит от контейнера db, так же добавляем в сеть project.

db

В этом контейнере будем использовать образ СУБД Percona версии 5.7. В блоке ports мы смаппим порт 3306 на хостовую машину, на порт, который будет определен в переменной MYSQL_PORT. Он будет использоваться для удобного подключения к СУБД напрямую. В блоке environment мы определим переменные окружения MYSQL_DATABASE и MYSQL_ROOT_PASSWORD. В MYSQL_DATABASE будет название базы данных, которая будет создана при первом запуске контейнера, в переменной MYSQL_ROOT_PASSWORD будет пароль для root пользователя. В блоке volumes мы смаппим содержимое папки /docker/mysql/conf.d (в ней будут конфиги) в /etc/percona-server.conf.d, папку /var/log/container из контейнера подключим к папке docker/log для удобного доступа к логам СУБД, а так же подключим созданный выше volume data-db в папку /var/lib/mysql контейнера и подключим к сети project. Привожу пример своих конфиг файлов, вы можете их изменить или дополнить:

bind.cnf

[mysqld]
bind-address="0.0.0.0"

charset.cnf

[mysqld]
init_connect='SET collation_connection = utf8_unicode_ci'
init_connect='SET NAMES utf8'
character-set-server=utf8
collation-server=utf8_unicode_ci
skip-character-set-client-handshake

[client]
default_character_set = utf8

[mysql]
default-character-set = utf8

log.cnf

[mysqld]
general_log = on
general_log_file = /var/log/container/mysql.query.log

slow_log.cnf

[mysqld]
long_query_time=5
log_queries_not_using_indexes=on
slow_query_log=1
slow_query_log_file=/var/log/container/mysql.slow_query.log

sql_mode.cnf

[mysqld]
sql_mode = NO_ENGINE_SUBSTITUTION,STRICT_ALL_TABLES

Окружение

Поскольку мы используем переменные окружения, нам будет необходимо создать файл .env в корне проекта, и указать в нем используемые переменные. Пример:

HTTP_PORT=80
MYSQL_PORT=3306
MYSQL_DATABASE=dev
MYSQL_PASSWORD=dev

Запуск

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

docker-compose build

Сборку контейнеров необходимо производить только один раз, пересобирать необходимо только в случае изменений в Dockerfile либо конфигурационных файлов php (которые копируются в контейнер на этапе сборки). Конфигурационные файлы можно и не копировать, а так же маппить в контейнер, как это сделано в контейнерах nginx и db, тут кому как удобней.

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

docker-compose up

Будут выкачены отсутствующие образы, созданы контейнеры, сети и все запущено. После запуска вы увидите вывод в stdout приложений запущенных в контейнерах. В основном это будет вывод от контейнера Percona. При таком запуске, консоль будет занята запущенными контейнерами, и в случае нажатия Ctrl + C или закрытия консоли, они будут остановлены. Чтобы это не происходило, и контейнеры запускались в фоне, необходимо добавить ключ -d к команде запуска:

docker-compose up -d

Я рекомендую при первом запуске использовать команду без ключа -d, так вы сможете убедиться, что запуск прошел нормально, и ни один из контейнеров не упал. В случае падения, вы скорее всего увидите в выводе ошибки, которые привели к краху контейнера, и сможете исправить их. Последующие запуски можно уже проводить сразу с ключом -d.

Когда работа над проектом завершена, можно остановить запущенные контейнеры командой:

docker-compose stop

Есть ещё так же команда docker-compose down, она отличается от stop тем, что помимо остановки, так же удаляет контейнеры и сети.

Выполнение команд в контейнере

Очень часто при разработке необходимо выполнять различные команды. Например запустить установку зависимостей composer, загрузить дамп базы в СУБД или накатить миграции. Сделать это можно с помощью команды docker-compose run. Например, с помощью следующей команды мы сможем подключиться к интерпретатору bash в контейнере php:

docker-compose run --rm php bash

И выполнять любые команды внутри этого контейнера. Например устанавливать зависимости composer. Вместо bash мы можем указать название любого исполняемого файла, который будет запущен внутри контейнера. Например, запустим установку зависимостей composer:

docker-compose run --rm php composer install

или запустим загрузку дампа базы данных:

docker-compose run --rm db mysql --host=db -u root -pdev dev < file.sql

Так же можно запустить накатывание миграций, пример для фреймворка Yii2:

docker-compose run --rm php php yii migrate

Настройка web приложения

Наше приложение требует подключения к базе данных, и его необходимо настроить, и у многих на этом месте возникают проблемы. Дело в том, что если в качестве хоста мы укажем localhost, то при подключении к БД из PHP будет ошибка, т.к. СУБД запущена в другом контейнере. Поэтому мы вместо localhost указываем доменное имя контейнера внутри Docker сети, в нашем случае это db:

<?php

return [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=db;dbname=dev',
    'username' => 'root',
    'password' => 'dev',
    'charset' => 'utf8',

    // Schema cache options (for production environment)
    //'enableSchemaCache' => true,
    //'schemaCacheDuration' => 60,
    //'schemaCache' => 'cache',
];

И теперь наше приложение будет подключаться к другому контейнеру, в котором находится СУБД. В СУБД нам нужно открыть возможность принимать подключения с любого адреса (по умолчанию принимаются подключения только с localhost). Но мы уже это сделали, добавив конфиг docker/mysql/conf.d/bind.cnf

Демо проект

Все приведенные выше конфигурационные файлы, а так же демо приложение на фреймворке yii2 я загрузил на github: https://github.com/lan143/docker-yii2. Вы можете скачать его и опробовать на своей машине.

Послесловие

Вы можете использовать не только 3 контейнера приведенные выше, но так же и подключать другие, которые необходимы для вашего проекта. Например Redis, MongoDB, Elasticsearch и ещё кучу других. Практически для всего существующего сейчас ПО существуют образы, которые можно запустить в docker контейнерах. Найти их можно на https://hub.docker.com/ Там так же обычно есть информация по подключению контейнеров и тонкостях их настройки. В последующих статьях я постараюсь описать подключение наиболее интересных и полезных контейнеров в веб разработке на PHP.

PS: Все файлы с конфигурацией обязательно должны иметь Linux формат конца строки (LF). Иначе 100% будут проблемы с запуском PHP контейнера, и могут быть проблемы с остальными контейнерами.