Мое dev окружение для PHP проектов
Разработка проектов требует не только навыков программирования, но и умения настроить эффективное рабочее окружение. В этой статье я поделюсь своим опытом создания dev-окружения для PHP-проектов с использованием Docker. Я расскажу, как организовать файловую структуру, настроить Dockerfile
и docker-compose.yml
, а также поделюсь полезными советами по оптимизации и безопасности. Независимо от того, работаете ли вы над pet-проектом или готовитесь к командной разработке, эти практики помогут вам создать надежную и гибкую среду разработки.
В основу этого окружения легли мои любимые и часто используемые инструменты:
- Docker для контейнеризации
- Docker compose для управления контейнерами
- PHP последней версии
- Composer для управления зависимостями PHP
- xDebug для отладки приложения
- RoadRunner для запуска приложения в режиме Long-Running
- PHP-фреймворк Yii3
- БД PostgreSQL, т.к. она умеет в keep-alive коннекты, в отличие от некоторых.
К сожалению, я так и не оформил привычное мне окружение для разработки в шаблон, который можно было бы использовать в команде 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 /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 ./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 ./ ./
RUN composer du --classmap-authoritative
CMD ["./rr", "serve"]
В нем происходит следующее:
- Ставим все необходимые системные пакеты с помощью
apt
,docker-php-ext
иpecl
, включаем расширения PHP.- Заметьте: xDebug я устанавливаю, но не включаю. Включу и настрою я его отдельным файлом настроек чуть ниже. Поступаю я так для того, чтобы на проде он не работал ни при каких условиях, включая невнимательность.
- Устанавливаем
composer
- Добавляем конфиг PHP для прода
- Создаём пользователя, под которым будет работать приложение. Это важно, т.к. внутри контейнера будут создаваться и изменяться файлы (пакеты композера, логи, результаты изменения от phpcsfixer и пр.), и нам нужно, чтобы пользователь с хост-системы (мы сами) мог читать и редактировать эти файлы. Значения id пользователя и группы мы принимаем в виде аргументов, чтобы была возможность установить их разными для разных систем.
- Отдельно добавляем в контейнер файлы
composer.json
,composer.lock
иconfiguration.php
(конфиг дляyiisoft/config
), да ставим зависимости с помощьюcomposer install
. И сразу скачиваем бинарник RoadRunner. Делаем этот шаг до копирования файлов проекта, т.к. докер закеширует результат и не будет повторять эти действия, пока два исходных файла не поменяются, т.е. пока список зависимостей и их версий остается неизменным. - Добавляем все файлы проекта
- Делаем
composer du --classmap-authoritative
. Этот шаг преследует сразу две цели:- Очевидная: после добавления файлов проекта мы оптимизируем скорость работы автолоудера.
- Неочевидная: по хуку команды
dump-autoload
генерируется карта мержей для yiisoft/config. Если есть вопросы по его содержимому - всегда рад ответить на них в своем телеграм-канале.
Конфиг 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
, оптимизация автолоудера) и удобству. Эти аспекты часто упускаются из виду, особенно на ранних этапах проекта, но они критически важны для долгосрочного успеха.
Надеюсь, что мой опыт и подход, описанные в этой статье, помогут вам не только быстрее настраивать окружение для ваших проектов, но и переосмыслить сам процесс организации разработки. Помните, что каждый проект уникален, и не бойтесь экспериментировать и адаптировать эти практики под свои нужды.
Сегодня я прошелся по верхам, не углубляясь. Здесь затронуто очень много тем, копать вглубь которых можно очень долго. Пишите мне о том, что из этого вам интересно, ставьте лайки в соцсетях, и на такие темы я опубликую больше контента. А связаться со мной можно в телеграм-канале. Давайте вместе делать процесс разработки более эффективным и приятным!