Каждый PHP-разработчик сталкивался с проблемой «у меня на компьютере работает, а на сервере — нет». Docker Compose решает это раз и навсегда: вы описываете окружение в одном YAML-файле и запускаете его одной командой. В этой статье соберём полноценный стек Nginx + PHP-FPM + MySQL для локальной разработки.
Что получим в итоге
После выполнения всех шагов у вас будет рабочее окружение:
- Nginx — проксирует запросы к PHP-FPM
- PHP 8.3-FPM — с нужными расширениями (pdo_mysql, mbstring, gd, zip, opcache)
- MySQL 8.0 — с сохранением данных между перезапусками
- Hot-reload — изменения в коде видны сразу без пересборки
Всё это — изолированно от вашей хост-системы. Можно держать несколько проектов с разными версиями PHP одновременно.

Структура проекта
Создайте следующую структуру каталогов. Именно она будет использоваться во всех примерах далее:
mkdir -p my-project/{src,nginx,docker}
cd my-project
# Итоговая структура:
# my-project/
# ├── docker-compose.yml
# ├── docker/
# │ └── php/
# │ └── Dockerfile
# ├── nginx/
# │ └── default.conf
# └── src/
# └── index.php
Каталог src/ — корень вашего PHP-проекта. Он монтируется в контейнеры Nginx и PHP-FPM как общий том.
docker-compose.yml — главный файл
Это ядро всей конфигурации. Три сервиса, одна сеть, один именованный том для данных MySQL:
# docker-compose.yml
services:
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
volumes:
- ./src:/var/www/html # код проекта
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf # конфиг
depends_on:
- php
networks:
- app-network
php:
build:
context: ./docker/php
dockerfile: Dockerfile
volumes:
- ./src:/var/www/html # тот же каталог, что и у Nginx
environment:
- DB_HOST=mysql
- DB_NAME=app_db
- DB_USER=app_user
- DB_PASS=secret
depends_on:
mysql:
condition: service_healthy # ждём готовности MySQL
networks:
- app-network
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: app_db
MYSQL_USER: app_user
MYSQL_PASSWORD: secret
volumes:
- db_data:/var/lib/mysql # данные переживут пересоздание контейнера
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 10
networks:
- app-network
volumes:
db_data:
networks:
app-network:
driver: bridge
Обратите внимание на ключевые моменты:
depends_onсcondition: service_healthy— PHP-контейнер не стартует, пока MySQL не примет соединения. Без этого вы получите ошибки «Connection refused» при первом запуске.db_data— именованный том. Данные MySQL сохраняются, даже если вы сделаетеdocker compose down. Чтобы удалить и данные, используйтеdocker compose down -v.- Сервис
phpне имеет проброшенных портов — он доступен только внутри Docker-сети через порт 9000 (FastCGI).
Dockerfile для PHP-FPM
Базовый образ php:8.3-fpm содержит минимум расширений. Для реальной разработки нужны дополнительные. Создайте docker/php/Dockerfile:
# docker/php/Dockerfile
FROM php:8.3-fpm
# Системные зависимости для расширений
RUN apt-get update && apt-get install -y \
libpng-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
libzip-dev \
unzip \
git \
&& rm -rf /var/lib/apt/lists/*
# PHP-расширения
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
pdo_mysql \
mbstring \
gd \
zip \
opcache \
intl
# Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Настройка OPcache для разработки
RUN echo "opcache.enable=1\n\
opcache.revalidate_freq=0\n\
opcache.validate_timestamps=1\n\
opcache.max_accelerated_files=10000\n\
opcache.memory_consumption=128" > /usr/local/etc/php/conf.d/opcache.ini
# Настройка PHP для разработки
RUN echo "display_errors=On\n\
error_reporting=E_ALL\n\
upload_max_filesize=64M\n\
post_max_size=64M\n\
memory_limit=256M" > /usr/local/etc/php/conf.d/dev.ini
WORKDIR /var/www/html
Утилита docker-php-ext-install — встроенный хелпер образов PHP. Она сама компилирует расширения и подключает их. Флаг -j$(nproc) ускоряет сборку за счёт всех ядер процессора.
Для продакшна достаточно изменить пару строк: отключить display_errors, поставить opcache.validate_timestamps=0 и убрать git из зависимостей.
Конфигурация Nginx
Создайте файл nginx/default.conf. Nginx принимает HTTP-запросы и передаёт PHP-файлы на обработку в PHP-FPM через FastCGI:
# nginx/default.conf
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php index.html;
# Логи — удобно для отладки
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Передача PHP-запросов в контейнер php
location ~ \.php$ {
fastcgi_pass php:9000; # имя сервиса из docker-compose
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
}
# Запрет доступа к скрытым файлам (.env, .git)
location ~ /\. {
deny all;
return 404;
}
# Кеширование статики
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
Ключевой момент — fastcgi_pass php:9000. Здесь php — это имя сервиса из docker-compose.yml. Docker автоматически резолвит его в IP-адрес контейнера внутри сети app-network.
Тестовый PHP-файл
Создайте src/index.php, чтобы проверить работу всего стека — веб-сервера, PHP и подключения к базе:
<?php
// src/index.php — проверка окружения
echo "<h1>Docker-окружение работает</h1>";
echo "<p>PHP " . PHP_VERSION . "</p>";
// Проверка подключения к MySQL
$host = getenv('DB_HOST') ?: 'mysql';
$db = getenv('DB_NAME') ?: 'app_db';
$user = getenv('DB_USER') ?: 'app_user';
$pass = getenv('DB_PASS') ?: 'secret';
try {
$pdo = new PDO("mysql:host={$host};dbname={$db};charset=utf8mb4", $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$version = $pdo->query('SELECT VERSION()')->fetchColumn();
echo "<p>MySQL: {$version} — подключение успешно</p>";
// Список загруженных расширений
echo "<h2>Расширения PHP</h2><ul>";
foreach (['pdo_mysql', 'mbstring', 'gd', 'zip', 'opcache', 'intl'] as $ext) {
$status = extension_loaded($ext) ? '✓' : '✗';
echo "<li>{$ext}: {$status}</li>";
}
echo "</ul>";
} catch (PDOException $e) {
echo "<p style='color:red'>Ошибка БД: " . $e->getMessage() . "</p>";
}
Запуск и управление
Всё готово. Запускаем:
# Первый запуск (сборка образа PHP + старт контейнеров)
docker compose up -d --build
# Проверяем статус
docker compose ps
# Смотрим логи всех контейнеров
docker compose logs -f
# Только логи PHP
docker compose logs -f php
Откройте http://localhost в браузере — вы увидите версии PHP и MySQL, а также список загруженных расширений.
Полезные команды для повседневной работы:
# Остановить контейнеры (данные MySQL сохраняются)
docker compose stop
# Остановить и удалить контейнеры (данные MySQL сохраняются)
docker compose down
# Остановить, удалить контейнеры И данные MySQL
docker compose down -v
# Пересобрать PHP-образ после изменения Dockerfile
docker compose build php
docker compose up -d
# Зайти внутрь PHP-контейнера (для отладки, запуска Composer и т.д.)
docker compose exec php bash
# Запустить Composer внутри контейнера
docker compose exec php composer install
# Выполнить PHP-скрипт
docker compose exec php php artisan migrate # для Laravel
docker compose exec php php bin/console cache:clear # для Symfony
Добавляем Xdebug для отладки
Для полноценной разработки пригодится Xdebug. Добавьте в Dockerfile перед строкой WORKDIR:
# Xdebug (только для разработки!)
RUN pecl install xdebug && docker-php-ext-enable xdebug
RUN echo "xdebug.mode=debug\n\
xdebug.client_host=host.docker.internal\n\
xdebug.client_port=9003\n\
xdebug.start_with_request=yes\n\
xdebug.idekey=PHPSTORM" > /usr/local/etc/php/conf.d/xdebug.ini
После пересборки (docker compose build php && docker compose up -d) Xdebug будет автоматически подключаться к PhpStorm или VS Code на порту 9003. Адрес host.docker.internal — специальный DNS, который Docker резолвит в IP хост-машины.
Переменные окружения и .env
Хранить пароли прямо в docker-compose.yml — плохая практика. Вынесите их в файл .env:
# .env (в корне проекта, рядом с docker-compose.yml)
MYSQL_ROOT_PASSWORD=rootpass
MYSQL_DATABASE=app_db
MYSQL_USER=app_user
MYSQL_PASSWORD=secret
PHP_VERSION=8.3
И используйте переменные в docker-compose.yml:
# Фрагмент docker-compose.yml
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
php:
build:
context: ./docker/php
args:
PHP_VERSION: ${PHP_VERSION:-8.3} # значение по умолчанию
Docker Compose автоматически подтягивает .env из текущей директории. Не забудьте добавить .env в .gitignore и создать файл .env.example с пустыми значениями для команды.
Чеклист для старта
- Убедитесь, что Docker Desktop (или Docker Engine + Compose plugin) установлен:
docker compose version - Создайте структуру проекта с 4 файлами:
docker-compose.yml,Dockerfile,default.conf,index.php - Запустите
docker compose up -d --build - Проверьте
http://localhost— должны отобразиться версии PHP и MySQL - Добавьте в
.gitignore:.env,db_data/,vendor/ - Для продакшна: отключите Xdebug,
display_errorsи включитеopcache.validate_timestamps=0
Это окружение подходит для любого PHP-фреймворка — Laravel, Symfony, Yii, WordPress. Достаточно положить код в src/ и при необходимости поправить root в конфиге Nginx (например, на /var/www/html/public для Laravel).
