PHP и Docker в CI/CD: построение воспроизводимой среды разработки и тестирования с нуля
Введение: проблема «works on my machine» в 2026 году
«У меня работает» — фраза, которая по-прежнему стоит командам недели отладки. Разные версии PHP, расширений, конфигурации nginx и переменных окружения между ноутбуком разработчика, стейджингом и продакшеном порождают баги, которые почти невозможно воспроизвести. В 2026 году, когда релизные циклы сократились до часов, а не дней, воспроизводимость среды — это не опция, а обязательное условие выживания команды.
Docker решает эту проблему на уровне инфраструктуры: один и тот же образ запускается на MacBook разработчика, в GitHub Actions и на продакшен-сервере. В связке с CI/CD-пайплайном это даёт полностью автоматизированный путь от коммита до деплоя с гарантированно идентичным окружением на каждом этапе. Эта статья — практическое руководство: никаких абстракций, только конкретные файлы и команды.
Базовая структура Docker-окружения для PHP-проекта
Типичный PHP-проект требует минимум трёх контейнеров: приложения (php-fpm), веб-сервера (nginx) и базы данных. Структура директорий задаёт тон всему проекту:
project/
├── docker/
│ ├── php/
│ │ ├── Dockerfile
│ │ └── php.ini
│ └── nginx/
│ └── default.conf
├── src/ # исходный код PHP
├── docker-compose.yml
├── docker-compose.override.yml # локальные переопределения
└── .env.exampleКлючевые принципы организации:
- Один процесс — один контейнер. php-fpm не занимается маршрутизацией, nginx не выполняет PHP.
- Volumes только для разработки. В продакшен-образе код должен быть встроен внутрь.
- Общая сеть. Все сервисы находятся в одной Docker-сети для изолированной коммуникации.
Написание правильного Dockerfile для PHP
Выбор базового образа
В 2026 году рекомендуемая база — php:8.3-fpm-alpine. Alpine даёт минимальный размер образа (~30 МБ против ~400 МБ у debian-вариантов) и меньшую поверхность атаки. Для продакшена избегайте тегов latest — фиксируйте минорную версию.
# docker/php/Dockerfile
FROM php:8.3-fpm-alpine3.19 AS base
# Системные зависимости
RUN apk add --no-cache \
git \
curl \
libpng-dev \
libzip-dev \
icu-dev \
oniguruma-dev \
&& docker-php-ext-install \
pdo_mysql \
mbstring \
zip \
gd \
intl \
opcache
# Устанавливаем Composer
COPY --from=composer:2.7 /usr/bin/composer /usr/bin/composer
# Настройки PHP
COPY docker/php/php.ini /usr/local/etc/php/conf.d/custom.ini
# Безопасность: не запускаем от root
RUN addgroup -g 1000 appgroup && adduser -u 1000 -G appgroup -s /bin/sh -D appuser
# Стадия для продакшена
FROM base AS production
WORKDIR /var/www/html
COPY --chown=appuser:appgroup src/ .
RUN composer install --no-dev --optimize-autoloader --no-interaction
USER appuser
# Стадия для разработки
FROM base AS development
RUN apk add --no-cache $PHPIZE_DEPS \
&& pecl install xdebug \
&& docker-php-ext-enable xdebug
WORKDIR /var/www/html
USER appuserНюансы безопасности
- Никогда не запускайте контейнер от
rootв продакшене — используйтеUSER appuser. - Не копируйте
.envв образ — передавайте переменные через среду запуска. - Добавьте
.dockerignore, чтобы исключитьnode_modules,.git, тесты из продакшен-образа. - Регулярно сканируйте образы через
docker scout cvesили Trivy.
Docker Compose для локальной разработки
# docker-compose.yml
services:
php:
build:
context: .
dockerfile: docker/php/Dockerfile
target: development
volumes:
- ./src:/var/www/html
- composer_cache:/root/.composer
environment:
APP_ENV: local
DB_HOST: db
DB_PORT: 3306
DB_DATABASE: ${DB_DATABASE}
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
db:
condition: service_healthy
networks:
- app_network
nginx:
image: nginx:1.27-alpine
ports:
- "8080:80"
volumes:
- ./src:/var/www/html:ro
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- php
networks:
- app_network
db:
image: mysql:8.4
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app_network
volumes:
db_data:
composer_cache:
networks:
app_network:
driver: bridgeОбратите внимание на healthcheck для базы данных. Без него PHP-контейнер стартует раньше, чем MySQL принимает соединения, и приложение падает при запуске. Опция condition: service_healthy в depends_on решает эту проблему на уровне Docker Compose.
Интеграция с CI/CD: пайплайн на GitHub Actions
GitHub Actions — де-факто стандарт для PHP Docker CI/CD в open-source и большинстве коммерческих проектов. Пайплайн состоит из четырёх этапов: линтинг, тесты, сборка образа, публикация в registry.
# .github/workflows/ci.yml
name: PHP CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
lint:
name: Code Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: PHP CS Fixer
run: |
docker run --rm \
-v ${{ github.workspace }}/src:/app \
cytopia/php-cs-fixer:3 fix --dry-run --diff /app
test:
name: Unit & Integration Tests
runs-on: ubuntu-latest
needs: lint
services:
db:
image: mysql:8.4
env:
MYSQL_DATABASE: test_db
MYSQL_USER: test_user
MYSQL_PASSWORD: test_pass
MYSQL_ROOT_PASSWORD: root_pass
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build test image
uses: docker/build-push-action@v5
with:
context: .
file: docker/php/Dockerfile
target: development
load: true
tags: php-app:test
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run PHPUnit
run: |
docker run --rm \
--network host \
-e APP_ENV=testing \
-e DB_HOST=127.0.0.1 \
-e DB_DATABASE=test_db \
-e DB_USERNAME=test_user \
-e DB_PASSWORD=test_pass \
php-app:test \
./vendor/bin/phpunit --coverage-text
build-and-push:
name: Build & Push Image
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: docker/php/Dockerfile
target: production
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=maxКэширование слоёв Docker в CI
Без кэширования каждая сборка в CI скачивает все зависимости заново — это 3–5 минут потерянного времени на каждый пуш. В GitHub Actions доступны два основных подхода:
- GitHub Actions Cache (
type=gha) — встроенный механизм, не требует сторонних сервисов. Рекомендуется для большинства проектов. - Registry Cache (
type=registry) — хранит кэш прямо в container registry. Подходит для self-hosted runners без общего кэша.
Ключевая стратегия для PHP — правильный порядок слоёв в Dockerfile. Сначала копируйте composer.json и composer.lock, устанавливайте зависимости, и только потом копируйте исходный код. Это гарантирует, что слой с vendor/ будет инвалидирован только при изменении зависимостей, а не при каждом изменении кода.
# Оптимизированная часть Dockerfile для кэширования
WORKDIR /var/www/html
# Сначала только файлы зависимостей
COPY src/composer.json src/composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts --no-interaction
# Потом весь код (этот слой инвалидируется чаще)
COPY src/ .
RUN composer run-script post-install-cmdПараллельный запуск тестов в Docker внутри пайплайна
Для крупных тест-сьютов (1000+ тестов) последовательный запуск — узкое место. GitHub Actions позволяет разбить тесты на группы через matrix-стратегию:
test-parallel:
name: Tests (shard ${{ matrix.shard }}/${{ matrix.total }})
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
total: [4]
steps:
- uses: actions/checkout@v4
- name: Run PHPUnit shard
run: |
docker run --rm \
-e APP_ENV=testing \
php-app:test \
./vendor/bin/phpunit \
--testsuite=Unit \
--group=shard${{ matrix.shard }}Альтернатива — использовать paratest внутри одного контейнера для параллелизации через процессы. Для PHP Docker CI/CD это часто быстрее, чем несколько матричных джобов из-за overhead на запуск контейнеров.
Управление переменными окружения и секретами
Один из главных источников инцидентов — утечка секретов через Docker-образы или логи CI. Правила работы с секретами в PHP Docker CI/CD:
- Никогда не добавляйте
.envв Docker-образ. Добавьте его в.dockerignore. - В GitHub Actions храните секреты в Repository Secrets или Environment Secrets (для раздельного управления стейджингом и продакшеном).
- Передавайте секреты в контейнер через
-e KEY=${{ secrets.KEY }}, не через build arguments. - Для сложных проектов используйте HashiCorp Vault или AWS Secrets Manager с динамической выдачей токенов.
- name: Run migrations
run: |
docker run --rm \
-e APP_KEY=${{ secrets.APP_KEY }} \
-e DB_PASSWORD=${{ secrets.DB_PASSWORD }} \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
php artisan migrate --forceBuild secrets (через
--secret id=...в Buildkit) позволяют передавать чувствительные данные во время сборки образа без их попадания в финальные слои. Используйте этот механизм для Composer с приватными репозиториями.
Артефакты и версионирование образов
Правильное версионирование образов — основа надёжного rollback. Рекомендуемая стратегия тегирования:
latest— всегда указывает на последний стабильный образ из веткиmain.sha-<commit-hash>— неизменяемый тег для точного воспроизведения любого деплоя.v1.2.3— семантическая версия из Git-тега для релизов.develop-<date>— временные образы для тестирования feature-веток.
Для rollback в продакшен достаточно изменить тег образа на предыдущий SHA и перезапустить сервис. Никогда не полагайтесь только на latest для rollback — это антипаттерн.
Типичные ошибки и как их избежать
- Xdebug в продакшен-образе. Подключайте Xdebug только в
developmentстадии Dockerfile. Он снижает производительность в 3–5 раз. - Монтирование
vendor/через volume в CI. Это ломает кэш слоёв и замедляет пайплайн. В CI зависимости должны быть внутри образа. - Использование
docker-compose upв CI без явных команд. Всегда используйте конкретные сервисы и команды, не запускайте весь стек целиком. - Отсутствие
.dockerignore. Без него контекст сборки включаетnode_modules(сотни МБ),.gitи тестовые данные. - Хранение секретов в переменных окружения образа (
ENV). Они видны черезdocker inspect. Передавайте секреты только в рантайме. - Разные версии PHP между локальной средой и CI. Фиксируйте точную версию образа в обоих местах, используйте одну переменную
PHP_VERSION.
Чеклист готового PHP CI/CD с Docker
- Dockerfile использует multi-stage build с отдельными стадиями
developmentиproduction. - Базовый образ зафиксирован до минорной версии (например,
php:8.3.10-fpm-alpine3.19). - Контейнер запускается не от
root, создан непривилегированный пользователь. - Файл
.dockerignoreисключает.git,node_modules,.env, тестовые файлы. - Docker Compose настроен с
healthcheckдля всех сервисов с состоянием (БД, Redis). - CI-пайплайн включает этапы: lint → test → build → push.
- Кэширование слоёв настроено через
type=ghaвdocker/build-push-action. - Зависимости (composer.json) копируются отдельным слоем до исходного кода.
- Секреты передаются в рантайме через переменные среды, не через
ENVв Dockerfile и не через build args. - Образы тегируются SHA коммита и семантической версией,
latestне используется для деплоя в продакшен. - Rollback задокументирован и протестирован: смена тега + перезапуск сервиса.
- Параллельный запуск тестов настроен для тест-сьютов более 500 тестов.
Воспроизводимая среда разработки с PHP Docker CI/CD — это не разовая настройка, а живая система. Регулярно обновляйте базовые образы, следите за CVE через автоматические сканеры и ревьюйте пайплайн при изменении архитектуры проекта. Команда, которая один раз правильно настроила эту инфраструктуру, экономит часы на каждом спринте.
Технологии
Теги
Руслан Исмаилов
Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →