Мое dev окружение для PHP проектов

September 30, 2024
Около 6 мин

Разработка проектов требует не только навыков программирования, но и умения настроить эффективное рабочее окружение. В этой статье я поделюсь своим опытом создания dev-окружения для PHP-проектов с использованием Docker. Я расскажу, как организовать файловую структуру, настроить Dockerfile и docker-compose.yml, а также поделюсь полезными советами по оптимизации и безопасности. Независимо от того, работаете ли вы над pet-проектом или готовитесь к командной разработке, эти практики помогут вам создать надежную и гибкую среду разработки.

Dev окружение с Docker (generated by ChatGPT)

В основу этого окружения легли мои любимые и часто используемые инструменты:

К сожалению, я так и не оформил привычное мне окружение для разработки в шаблон, который можно было бы использовать в команде composer create project. Но это и не очень удобно, т.к. от проекта к проекту шаблон изменяется, эволюционирует. В каждый следующий проект я беру версию из предыдущего. Однако, основные принципы уже давно не меняются, и сегодня я покажу вам в первую очередь их.

Dockerfile и сохранение стейта

Во-первых, в корне каждого проекта у меня есть папка .docker, куда я скидываю все специфичные для него вещи:

  • Dockerfile для образов контейнеров PHP и других
  • Конфиги, которые зашиваются внутрь образов контейнеров или подключаются томами. Например, основной php.ini добавляется внутрь образа, а включение xDebug подключается томом только в среде локальной разработки
  • Стейт контейнеров. Я всегда сохраняю на диск стейт БД (в проде иначе и не получится), а в dev-окружении - ещё и кеш композера, чтобы его команды выполнялись быстрее. Вот пример структуры из последнего моего проекта, которую я коммичу в репозиторий:
.docker
├──data
│  ├──composer
│     └──.gitignore
│  └──postgres
│     └──.gitignore
└──php
   ├──Dockerfile
   ├──php.ini
   └──xdebug.ini
  • Папка data - для сохранения стейта из контейнеров между их перезапусками. Внутри каждой подпапки лежит файл .gitignore с двумя строками: * для игнора всего в этой папке и !.gitignore для того, чтобы сам этот файл закоммитить.
    *
    !.gitignore
    
  • Папка php содержит Dockerfile и конфиги для контейнера с PHP
  • Если мне понадобится билдить еще какие-то контейнеры - я создам еще одну папку рядом, и в нее тоже положу Dockerfile и все необходимые конфиги.

А вот - стартовый вариант моего Dockerfile:

FROM php:8.3-cli-bullseye

RUN apt update \
    && apt install -y  \
        libicu-dev \
        linux-headers-generic \
        zip \
        libpq-dev \
    && apt install -y $PHPIZE_DEPS \
    && docker-php-ext-configure intl \
    && docker-php-ext-install -j$(nproc) \
        pdo \
        pdo_pgsql \
        pgsql \
        pcntl \
        sockets \
        intl \
        opcache \
    && pecl install xdebug \
    && docker-php-ext-enable intl \
    && pecl clear-cache \
    && apt purge -y $PHPIZE_DEPS \
    && apt clean

COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

ADD .docker/php/php.ini /usr/local/etc/php/conf.d/40-custom.ini

WORKDIR /var/www

ARG USER_ID=1000
ARG GROUP_ID=1000

RUN groupadd -g $GROUP_ID appuser && \
    useradd -m -N -g $GROUP_ID -u $USER_ID appuser && \
    usermod -L appuser && \
    chown appuser:appuser .

USER appuser

COPY --chown=appuser:appuser ./composer.* configuration.php ./

RUN composer install \
        --prefer-dist \
        --no-ansi \
        --no-dev \
        --no-interaction \
        --no-plugins \
        --no-progress \
        --no-scripts \
    && vendor/bin/rr get --no-config -q -n

COPY --chown=appuser:appuser ./ ./
RUN composer du --classmap-authoritative

CMD ["./rr", "serve"]

В нем происходит следующее:

  1. Ставим все необходимые системные пакеты с помощью apt, docker-php-ext и pecl, включаем расширения PHP.
    1. Заметьте: xDebug я устанавливаю, но не включаю. Включу и настрою я его отдельным файлом настроек чуть ниже. Поступаю я так для того, чтобы на проде он не работал ни при каких условиях, включая невнимательность.
  2. Устанавливаем composer
  3. Добавляем конфиг PHP для прода
  4. Создаём пользователя, под которым будет работать приложение. Это важно, т.к. внутри контейнера будут создаваться и изменяться файлы (пакеты композера, логи, результаты изменения от phpcsfixer и пр.), и нам нужно, чтобы пользователь с хост-системы (мы сами) мог читать и редактировать эти файлы. Значения id пользователя и группы мы принимаем в виде аргументов, чтобы была возможность установить их разными для разных систем.
  5. Отдельно добавляем в контейнер файлы composer.json, composer.lock и configuration.php (конфиг для yiisoft/config), да ставим зависимости с помощью composer install. И сразу скачиваем бинарник RoadRunner. Делаем этот шаг до копирования файлов проекта, т.к. докер закеширует результат и не будет повторять эти действия, пока два исходных файла не поменяются, т.е. пока список зависимостей и их версий остается неизменным.
  6. Добавляем все файлы проекта
  7. Делаем composer du --classmap-authoritative. Этот шаг преследует сразу две цели:
    1. Очевидная: после добавления файлов проекта мы оптимизируем скорость работы автолоудераopen in new window.
    2. Неочевидная: по хуку команды dump-autoload генерируется карта мержей для yiisoft/configopen in new window. Если есть вопросы по его содержимому - всегда рад ответить на них в своем телеграм-каналеopen in new window.

Конфиг docker compose

Во-вторых, в репозитории с проектом всегда есть файл docker-compose.yml. Да, мне пора его переименовать в compose.yaml под новый стандарт, но я все забываю 😅 Вот его примерное must have содержимое:

version: "3.8"  
  
services:  
  php:  
    depends_on:  
      db:  
        condition: service_healthy
    build:  
      dockerfile: .docker/php/Dockerfile  
      context: ./  
      args:  
        USER_ID: ${USER_ID:-1000}  
        GROUP_ID: ${GROUP_ID:-1000}  
    command: "./rr serve -c .rr.dev.yaml"  
    user: "${USER_ID:-1000}:${GROUP_ID:-1000}"  
    restart: unless-stopped
    ports:
      - ${WEB_PORT:-80}:80
    volumes:
      - ./.docker/php/php.ini:/usr/local/etc/php/conf.d/40-custom.ini:ro
      - ./.docker/php/xdebug.ini:/usr/local/etc/php/conf.d/99-xdebug.ini:ro
      - ./.docker/data/composer:/home/appuser/.composer
      - ./:/var/www
    environment:
      TZ: Asia/Almaty
      PHP_IDE_CONFIG: ${PHP_IDE_CONFIG:-}
      DB_NAME: ${DB_NAME}
      DB_HOST: ${DB_HOST}
      DB_PORT: ${DB_PORT}
      DB_LOGIN: ${DB_LOGIN}
      DB_PASSWORD: ${DB_PASSWORD}
      XDEBUG_MODE: ${XDEBUG_MODE:-off}
      XDEBUG_CONFIG: "client_host=host.docker.internal"
      XDEBUG_TRIGGER: "yes"
  
  db:
    image: postgres:15-alpine
    restart: unless-stopped
    volumes:
      - .docker/data/postgres/db:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD:-dev}
      POSTGRES_USER: ${DB_USER:-dev}
      POSTGRES_DB: ${DB_NAME:-dev}
    ports:
      - "${DB_PORT:-5432}:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-dev}"]
      interval: 3s
      timeout: 3s
      retries: 10

Из этого примера видно, что я активно пользуюсь переменными окружения. Их значения задаются в файле .env, который добавлен в .gitignore. В репозиторий вместо него я коммичу .env.example, который содержит все возможные переменные окружения с пустыми или фейковыми значениями. Благодаря такому подходу приложение зависит от переменных окружения, а не от способов их добавления. Ну а вот - пример моего .env.example:

##################################
# Port forwarding
##################################
WEB_PORT=80

##################################
# xDebug settings
##################################
# Uncomment next line for xDebug usage with PhpStorm
# PHP_IDE_CONFIG=serverName=docker
# XDEBUG_MODE=develop,debug

# LINUX ZONE. Next configs are for xDebug in linux environment only.
# COMPOSE_FILE=docker-compose.yml:docker-compose.linux.yml
# Uncomment HOST_IP and set host ip in the docker network when using linux, look at docker-compose.linux.yml
# Although it usually is 172.17.0.1, your real value can be easily found with command `ip address | grep docker`,
# you'll see it like "inet 172.17.0.1/16 ..."
# HOST_IP=172.17.0.1

##################################
# Database settings
##################################
DB_HOST=db
DB_PORT=5432
DB_NAME=dev
DB_LOGIN=dev
DB_PASSWORD=dev

Если вы не знаете, зачем нужны те или иные настройки xDebug - рекомендую прочесть мой пример настройки xDebug в докере.

Особенности разработки под Linux

Если вы ведете разработку на Linux и не используете Docker Desktop (а я именно так и делаю), то внутри контейнеров вам недоступен хост host.docker.internal. И чтобы была возможность пользоваться xDebug (ему ведь надо самостоятельно коннектиться к IDE из контейнера) всегда добавляю в docker-compose.yml в сервис с PHP этот блок:

extra_hosts:
  - host.docker.internal:${HOST_IP:-172.17.0.1}

А когда я планирую работать над проектом не один, то вытаскиваю этот блок в соседний файл, который обычно называю docker-compose.linux.yml, в основном же файле остаётся все остальное. И чтобы каждый раз не писать в каждой команде список подключаемых конфигов (например, docker compose -f docker-compose.yml -f docker-compose.linux.yml run --rm php php -v), в .env я также добавляю такую переменную окружения:

COMPOSE_FILE=docker-compose.yml:docker-compose.linux.yml

Запуск

В первую очередь, конечно, добавляем composer.json и прочие файлы проекта. А непосредственно запуск такого окружения прост до банальности:

docker compose up -d

При первом запуске контейнер сначала сбилдится. Это может занять больше 5 минут в зависимости от мощности машины и качества канала в интернет. А когда он окончательно запустится - все готово к работе и отладке.

Совет

Небольшой совет: чтобы быть увереным, что проект заведется на проде, добавляйте/обновляйте php-зависимости тоже через это окружение, а не локально установленным composer'ом:

docker composer run --rm --no-deps php composer require foo/bar

Если у какого-то пакета будет зависимость от окружения (например, необходимо расширение PHP pcntl, которое вы себе решили не ставить в Dockerfile), вы сразу увидите, в чем проблема. Также это поможет поставить именно те версии пакетов, которые совместимы с вашим окружением, если такие доступны.

Заключение

Создание эффективного dev-окружения - это не просто настройка инструментов, а эволюционный процесс. Мой подход, который я описал в этой статье, сформировался за годы работы над различными проектами, от пет-проектов до крупных командных разработок.

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

Использование Docker, тщательно продуманная структура .docker директории, гибкие настройки через переменные окружения - все это позволяет достичь этого баланса. Мы получаем стабильное окружение, которое при этом легко кастомизировать.

Особое внимание я уделил оптимизации производительности (кеширование данных, грамотное построение слоев в Dockerfile, оптимизация автолоудера) и удобству. Эти аспекты часто упускаются из виду, особенно на ранних этапах проекта, но они критически важны для долгосрочного успеха.

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

Сегодня я прошелся по верхам, не углубляясь. Здесь затронуто очень много тем, копать вглубь которых можно очень долго. Пишите мне о том, что из этого вам интересно, ставьте лайки в соцсетях, и на такие темы я опубликую больше контента. А связаться со мной можно в телеграм-каналеopen in new window. Давайте вместе делать процесс разработки более эффективным и приятным!