27 October 2019

Docker + php-fpm + PhpStorm + Xdebug

PHP
Tutorial
Не так давно тимлид нашей команды сказал: ребята я хочу, чтобы у всех была одинаковая среда разработки для наших боевых проектов + мы должны уметь дебажить всё — и web приложения, и api запросы, и консольные скрипты, чтобы экономить свои нервы и время. И поможет нам в этом docker.

Сказано — сделано. Подробности под катом.

В сети есть много мануалов по контейнеризации, но как их применить к реальной боевой разработке? Для каждого проекта написать свой docker-compose.yml? Но все наши проекты общаются между собой через апи, они все используют стандартный стек технологий: nginx + php-fpm + mysql.

Поэтому, давайте уточним условия задачи:

  1. Мы работаем в компании, в команде, сопровождаем несколько боевых проектов. Все работаем под Ubuntu + PhpStorm
  2. Для локальной разработки мы хотим использовать докер, для того, чтобы иметь одинаковую среду разработки у каждого члена команды, а также для того, чтобы когда придет новый разработчик, он смог быстро развернуть рабочее окружение
  3. Мы хотим разрабатывать с комфортом, мы хотим дебажить всё: и web приложения, и консольные скрипты, и api запросы.

Еще раз: мы хотим завести в докер несколько рабочих проектов.

На боевых серверах используется стандартная связка nginx + php-fpm + mysql. И, в чем проблема?

Разворачиваем на локальной машине точно такое же окружение + Xdebug, настраиваем наши проекты в PhpStorm, работаем. Для дебага включаем «трубку» в PhpStorm, всё работает из коробки, всё замечательно.



Всё это действительно так — всё работает из коробки. Но, давайте попробуем заглянуть под капот нашего рабочего окружения.

Nginx + php-fpm общаются через сокет, xdebug слушает порт 9000, PhpStorm тоже, по умолчанию, слушает порт 9000 для дебага и всё вроде бы замечательно. А если у нас открыто несколько приложений в PhpStorm, и включена прослушка («трубка)» для нескольких приложений? Что сделает PhpStorm? Он начнет ругаться, что обнаружено новое подключение для Xdebug, вы хотите его игнорировать, или нет?

Т.е., при настройках по умолчанию в PhpStorm, в конкретный момент времени, я могу дебажить только одно приложение. У всех других открытых приложений дебаг должен быть выключен. Блин, но это же неудобно. Я хочу слушать для дебага все приложения, и если в каком-то из них стоит точка останова, то я хочу, чтобы PhpStorm остановился в этом приложении, на той строке, где мне нужно.

А что для этого нужно? А нужно, чтобы каждое приложение запускалось со своими настройками для Xdebug. Чтобы каждое приложение слушало свой порт, искало свой сервер, а не так, как у нас всё общее, всё в одной куче.

А для этого есть замечательный докер! Мы можем запустить каждое наше боевое приложение в отдельном контейнере, на основе одного общего образа, например, php:7.1-fpm. Благодаря технологии докера мы можем изолировать наши приложения, при минимальных накладных расходах.

Ок, давайте заведем наши боевые проекты под докер, запустим каждый проект в отдельном контейнере, настроим каждый проект в PhpStorm для дебага индивидуально, всё должно быть замечательно.

И, упс, первая проблема: контейнеры в докере запускаются от имени суперпользователя root, а локально мы работаем, обычно, от пользователя с uid 1000, gid 1000. Приложения боевые, и давать каждому приложению права 777 на всё — это не выход. Наши же приложения под гитом, и если мы дадим права 777 локально, то гит всё это запишет, и передаст на боевой сервер.

Костылим, вот пример образа php:7.1-fpm, который будет собираться.

Update


Как справедливо указало сообщество — совсем уж жестко костылить всё-таки не надо.
Например хаброюзер 1ntrovert в своем комментарии

Первоначальный пример образа php:7.1-fpm (uid и gid жестко прописаны)
FROM php:7.1-fpm

RUN apt-get update && apt-get install -y \
        git \
        curl \
        wget \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libmcrypt-dev \
        libpng-dev zlib1g-dev libicu-dev g++ libmagickwand-dev libxml2-dev \
    && docker-php-ext-configure intl \
    && docker-php-ext-install intl \
    && docker-php-ext-install mbstring zip xml gd mcrypt pdo_mysql \
    && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
    && docker-php-ext-install -j$(nproc) gd \
    && pecl install imagick \
    && docker-php-ext-enable imagick \
    && pecl install xdebug \
    && docker-php-ext-enable xdebug

ADD ./php.ini /usr/local/etc/php/php.ini

RUN wget https://getcomposer.org/installer -O - -q \
    | php -- --install-dir=/bin --filename=composer --quiet

RUN usermod -u 1000 www-data && groupmod -g 1000 www-data

WORKDIR /var/www

USER 1000:1000

CMD ["php-fpm"]



Поправленный пример Dockerfile

FROM php:7.1-fpm

ARG USER_ID
ARG GROUP_ID

RUN apt-get update && apt-get install -y \
        git \
        curl \
        wget \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libmcrypt-dev \
        libpng-dev zlib1g-dev libicu-dev g++ libmagickwand-dev --no-install-recommends libxml2-dev \
    && docker-php-ext-configure intl \
    && docker-php-ext-install intl \
    && docker-php-ext-install mbstring zip xml gd mcrypt pdo_mysql \
    && pecl install imagick \
    && docker-php-ext-enable imagick \
    && pecl install xdebug-2.5.0 \
    && docker-php-ext-enable xdebug

ADD ./php.ini /usr/local/etc/php/php.ini

RUN wget https://getcomposer.org/installer -O - -q \
    | php -- --install-dir=/bin --filename=composer --quiet


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

WORKDIR /var/www

USER "${USER_ID}:${GROUP_ID}"

CMD ["php-fpm"]


При запуске контейнера из данного образа пользователь www-data получает uid=1000, gid=1000. Обычно такие права у первого созданного пользователя в операционной системе Линукс. И, именно с такими правами будут работать наши php-fpm контейнеры. Буду очень благодарен, если кто-то подскажет как можно работать без костылей с правами доступа в докер.

При запуске контейнера из данного образа пользователь www-data получает uid и gid, которые будут переданы извне.

Также в комментариях поднималась тема: зачем вообще менять права пользователю www-data, чем не устраивают стандартные права 33. Только одним: когда мы зайдем в контейнер, и создадим, например, файл миграции, то на машине хоста владельцем этого файла будем не мы. И каждый раз надо будет запускать что-то вроде
 sudo chown -R user:user ./


И вторая небольшая проблема: для корректной работы Xdebug необходимо прописать верный ip адрес для машины хоста. У каждого члена команды он разный. 127.0.0.1 не катит. И тут нам на помощь приходит сам докер. Например, мы можем явно сконфигурировать сеть — 192.168.220.0/28. И тогда наша машина всегда будет иметь адрес 192.168.220.1. Этот адрес мы будем использовать как для настройки PhpStorm, так и для настройки других приложений. Например, при работе с MySql.

Сам docker-compose.yml, после учета замечаний, выглядит так:

version: '3'
services:
    php71-first:
      build:
        context: ./images/php71
        args:
          - USER_ID
          - GROUP_ID
      volumes:
        - ./www:/var/www
        - ./aliases/php71/bash.bashrc:/etc/bash.bashrc
      environment:
        XDEBUG_CONFIG: "remote_host=192.168.220.1 remote_enable=1 remote_autostart=off  remote_port=9008"
        PHP_IDE_CONFIG: "serverName=first"
      networks:
        - test-network
    php71-two:
      build:
        context: ./images/php71
        args:
          - USER_ID
          - GROUP_ID
      volumes:
        - ./www:/var/www
        - ./aliases/php71/bash.bashrc:/etc/bash.bashrc
      environment:
        XDEBUG_CONFIG: "remote_host=192.168.220.1 remote_enable=1 remote_autostart=off  remote_port=9009"
        PHP_IDE_CONFIG: "serverName=two"
      networks:
        - test-network
    nginx-test:
      image: nginx
      volumes:
        - ./hosts:/etc/nginx/conf.d
        - ./www:/var/www
        - ./logs:/var/log/nginx
      ports:
        - "8080:80"
      depends_on:
        - php71-first
        - php71-two
      networks:
        test-network:
          aliases: # алиасы нужны если нужно общаться внутри сети между хостами. Например, если вы используете api
            - first.loc
            - two.loc
#    mysql:
#      image: mysql:5.7
#      ports:
#        - "3306:3306"
#      volumes:
#        - ./mysql/data:/var/lib/mysql
#      environment:
#        MYSQL_ROOT_PASSWORD: secret
#      networks:
#        - test-network
networks:
  test-network:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.220.0/28


Мы видим, что в данном конфиге создаются два контейнера php71-first и php71-two, на основе одного образа php:7.1-fpm. У каждого контейнера свои настройки для Xdebug. Каждый отдельно взятый контейнер будет слушать, для дебага, свой порт и свой сервер.

Также, обращаю ваше внимание на директивы
        args:
          - USER_ID
          - GROUP_ID


Без этих переменных образ php-fpm не запустится. Вопрос: как их передать в docker-compose.yml? Ответ: так как удобнее вам. Можно при запуске:
USER_ID=$(id -u) GROUP_ID=$(id -g) docker-compose up -d

Можно прописать эти переменные в файле .env, который лежит на одном уровне с файлом docker-compose.yml
USER_ID=1000
GROUP_ID=1000

Мне больше нравится вариант с .env файлом. Конечно-же можно использовать Makefile. Кому как больше нравится.

Полный код для демо версии выложен на гитхабе.

Листинг демо проекта:



Пробежимся, кратко, по листингу проекта.

Каталог aliases -> php71 -> bash.bashrc. Спорный момент. Я предпочитаю общаться с php-fpm контейнерами через алиасы.

Данный файл пробрасывается в docker-compose.yml: — ./aliases/php71/bash.bashrc:/etc/bash.bashrc
Стандартный инструмент Линукса.

Каталог hosts — конфигурационные файлы для Nginx. В каждом конфиге прописан свой контейнер php-fpm. Пример:

server {
    listen 80;
    index index.php;
    server_name first.loc;
    error_log  /var/log/nginx/first_error.log;
    root /var/www/first.loc;

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

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        # контейнер php-fpm
        fastcgi_pass php71-first:9000;
        fastcgi_index index.php;
        fastcgi_read_timeout 1000;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}

Каталог images — инструкции для сборки образов php-fpm, каталог mysql — храним базы, каталог www — все наши web проекты, в нашем примере first.loc и two.loc.

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

Осталось корректно настроить PhpStorm для каждого из проектов. При настройке мы должны прописать порт для дебага и имя сервера в нескольких местах.

Создаем проект в PhpStorm







Настраивать будем разделы меню
— PHP (необходимо верно прописать CLI Interpreter),
— Debug (меняем порт на 9008, как в файле docker-compose.yml),
— DBGp proxy (IDE key, Host, Port),
update Спасибо хаброюзеру CrazyLazy за важное замечание. Пункт меню DBGp proxy настраивать не надо.
— Servers (необходимо верно указать имя сервера, как в файле docker-compose.yml, и use path mappings)



Все дальнейшие скрины буду прятать под спойлер.

Настраиваем CLI Interpreter из docker-compose.yml файла
Хитрого ничего нет — важно, при настройке выбрать нужный образ, и верно прописать имя сервера. По умолчанию имя сервера Docker, у нас оно своё.



















Настраиваем раздел меню Debug
Опять же всё прописываем из настроек docker-compose.yml для конкретного контейнера. На этом же шаге валидируем как работает наш дебаг.






Настраиваем раздел меню Servers
Важно правильно прописать use path mappings, имя сервера опять же берем из настроек





Уходим из раздела меню File -> Settings, идем в раздел меню Run -> Edit Configuration, создаем Php Web Page
Сервер выбираем наш, созданный на предыдущем шаге.







Ну, собственно и всё. Написано много букв, вроде бы всё непросто


На самом деле — главное понять очень простую вещь. Благодаря технологии докера мы можем запустить все наши рабочие приложения в едином пространстве, но с разными настройками для Xdebug. Каждое приложение работает в своем контейнере, и нам остаётся аккуратно прописать настройки для каждого приложения в PhpStorm.

И на выходе мы получаем чудесную картину.

1. Клонируем репозиторий на гитхабе. Создаем .env файл с переменными
USER_ID=ваш uid
GROUP_ID=ваш gid


2. Прописываем узлы first.loc и two.loc в файле /etc/hosts

127.0.0.1 first.loc
127.0.0.1 two.loc

3. В папке с гитом запускаем команду docker-compose up -d

4. Настраиваем оба проекта first.loc и two.loc в PhpStorm, так как описано выше, и запускаем оба проекта в PhpStorm. Т.е. у нас открыто два окна PhpStorm, с двумя проектами, каждый из них слушает входящие соединения (трубка включена).

5. В проекте two.loc ставим точку останова на второй, например, строке. В первом проекте first.loc запускаем http запрос из файла http.http

И о чудо! Нас перекидывает во второй проект, на нашу точку останова.

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

Что-то вроде:

alex@alex-Aspire-ES1-572 ~ $ php71first 
www-data@a0e771cfac72:~$ cdf
www-data@a0e771cfac72:~/first.loc$ php index.php 
I'am first host
www-data@a0e771cfac72:~/first.loc$

Где php71first — алиас на машине хоста:

alias php71first="cd ~/docker_git && docker-compose exec php71-first bash"

cdf — алиас, который работает в контейнере. Выше я писал о том, что для общения с контейнерами предпочитаю использовать алиасы.

На этом всё, конструктивная критика, замечания приветствуются.

P.S. Хочется выразить огромную благодарность Денису Бондарю за его статью PhpStorm + Docker + Xdebug, которая была отправной точкой для написания данного туториала.
Tags:dockerphp-fpmnginxphpstormdebug
Hubs: PHP
+23
35.7k 220
Comments 80
Top of the last 24 hours