Docker для PHP: PHP-FPM, Nginx и MySQL в docker compose
Коротко
Удобнее всего запускать PHP в Docker связкой из трёх контейнеров: PHP-FPM выполняет код, Nginx принимает запросы и отдаёт статику, MySQL хранит данные. Их описывают в одном файле compose.yaml и поднимают командой docker compose up -d. Ниже готовая сборка docker php с Dockerfile, установкой расширений через docker-php-ext-install, Composer в контейнере и разбором частых ошибок.
Содержание
- Как устроено окружение Docker для PHP
- Что установить перед началом
- Структура проекта
- Dockerfile для PHP-FPM с расширениями
- Конфигурация Nginx для PHP-FPM
- Файл compose.yaml: PHP, Nginx и MySQL
- Запуск и основные команды docker compose
- Composer в контейнере
- phpMyAdmin в том же compose
- Частые ошибки
- Cron и фоновые задачи
Как устроено окружение Docker для PHP
Официальный образ php на Docker Hub выпускается в нескольких вариантах. Для связки с Nginx нужен php:8.x-fpm: в нём работает PHP-FPM, который слушает порт 9000 и выполняет скрипты по запросу веб-сервера. Вариант php:8.x-apache содержит Apache с mod_php и подходит, если проект завязан на .htaccess. Вариант php:8.x-cli нужен для консольных скриптов и воркеров.

Каждый сервис живёт в своём контейнере, а Docker Compose связывает их общей сетью. Внутри этой сети контейнеры обращаются друг к другу по имени сервиса: Nginx передаёт PHP-запросы на app:9000, а PHP подключается к базе по адресу db, а не localhost. Это главное отличие от XAMPP или Open Server, где всё работает на одной машине.
| Образ | Для чего | Когда выбрать |
|---|---|---|
php:8.5-fpm | PHP-FPM без веб-сервера | Связка с Nginx, основной вариант |
php:8.5-apache | Apache и PHP в одном контейнере | Старые проекты с .htaccess |
php:8.5-cli | Только командная строка | Cron, очереди, консольные утилиты |
*-alpine | Те же образы на Alpine Linux | Нужен минимальный размер образа |
В ноябре 2026 года актуальны ветки PHP 8.4 и 8.5, обе получают обновления. Берите ту, на которой работает ваш продакшен-сервер, чтобы локальная среда с ним совпадала.
Что установить перед началом
- Windows 10 и 11: Docker Desktop с движком WSL 2. Код проекта лучше держать внутри файловой системы WSL (
\\wsl$\Ubuntu\home\user\projects), а не на дискеC:: так файлы читаются в разы быстрее. - macOS: Docker Desktop для Apple Silicon или Intel.
- Ubuntu и другие Linux: Docker Engine и плагин Compose из официального репозитория Docker (пакеты
docker-ceиdocker-compose-plugin), инструкция на docs.docker.com.
Проверьте установку командами docker --version и docker compose version. Команда пишется через пробел: старая утилита docker-compose с дефисом больше не развивается, а в файлах Compose не нужна строка version: в начале.
Структура проекта
Разложите файлы так, чтобы настройки окружения лежали отдельно от кода:
myapp/
├── compose.yaml # описание всех сервисов
├── .env # пароли и порты (не коммитить)
├── docker/
│ ├── php/
│ │ ├── Dockerfile # образ PHP-FPM с расширениями
│ │ └── php.ini # свои настройки PHP
│ └── nginx/
│ └── default.conf # виртуальный хост Nginx
└── src/ # код проекта
├── composer.json
└── public/
└── index.php # точка входа
Dockerfile для PHP-FPM с расширениями
В базовом образе PHP собран с минимумом расширений: нет даже драйвера MySQL для PDO. Для установки в образ входят вспомогательные скрипты docker-php-ext-configure, docker-php-ext-install и docker-php-ext-enable.
# docker/php/Dockerfile
FROM php:8.5-fpm
# Системные библиотеки, без которых не соберутся intl, zip и gd
RUN apt-get update && apt-get install -y --no-install-recommends \
libicu-dev libzip-dev libpng-dev libjpeg62-turbo-dev libfreetype6-dev unzip git \
&& rm -rf /var/lib/apt/lists/*
# gd с поддержкой JPEG и шрифтов, затем остальные расширения
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j"$(nproc)" pdo_mysql mysqli intl zip gd bcmath
# Расширение из PECL: ставится через pecl и включается отдельно
RUN pecl install redis && docker-php-ext-enable redis
# Composer из официального образа
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Свои настройки PHP
COPY php.ini /usr/local/etc/php/conf.d/zz-app.ini
WORKDIR /var/www/html

Как работает docker-php-ext-install
Команда docker-php-ext-install pdo_mysql компилирует расширение из исходников PHP, лежащих в образе, и сразу создаёт файл включения в /usr/local/etc/php/conf.d/. Список того, что можно поставить так, выводит docker-php-ext-install без аргументов. Ключ -j"$(nproc)" собирает на всех ядрах и заметно ускоряет сборку.
- Ошибка вида
configure: error: Package requirements (icu-uc >= ...) were not metзначит, что не хватает системной-dev-библиотеки: для intl этоlibicu-dev, для ziplibzip-dev, для gdlibpng-dev. - Расширения из PECL (
redis,xdebug,imagick,apcu) ставятся черезpecl install, а включаются черезdocker-php-ext-enable. - В PHP 8.5 OPcache встроен в ядро и включается настройкой
opcache.enable; в образах 8.4 и старше его ставят командойdocker-php-ext-install opcache.
Если расширений много, проще утилита install-php-extensions: она сама ставит нужные системные библиотеки и работает одинаково на Debian и Alpine. Пример: install-php-extensions pdo_mysql intl zip gd redis.
Настройки php.ini в контейнере
В официальном образе нет рабочего php.ini, только два шаблона в $PHP_INI_DIR (/usr/local/etc/php): php.ini-development и php.ini-production. Для локальной разработки достаточно положить небольшой файл с нужными директивами в conf.d, как в Dockerfile выше:
; docker/php/php.ini
memory_limit = 256M
upload_max_filesize = 32M
post_max_size = 32M
date.timezone = Europe/Moscow
display_errors = On
error_reporting = E_ALL
Проверить, какие файлы подхватил PHP, можно командой docker compose exec app php --ini. Чем отличаются эти файлы на обычном сервере и где их искать, подробно в статье где находится php.ini.
Конфигурация Nginx для PHP-FPM
Nginx отдаёт картинки, CSS и JS сам, а запросы к .php передаёт в контейнер app по протоколу FastCGI. Корень сайта у обоих контейнеров должен указывать на одни и те же файлы.
# docker/nginx/default.conf
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php;
# Все адреса, кроме существующих файлов, идут в index.php
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app:9000; # имя сервиса из compose.yaml
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
}
# Скрытые файлы (.env, .git) наружу не отдаём
location ~ /\. {
deny all;
}
}
Файл compose.yaml: PHP, Nginx и MySQL
Теперь соберём сервисы вместе. Пароли вынесены в .env, Compose подставляет их сам.
# compose.yaml
services:
app:
build: ./docker/php
volumes:
- ./src:/var/www/html
environment:
DB_HOST: db
DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
db:
condition: service_healthy
web:
image: nginx:stable-alpine
ports:
- "8080:80"
volumes:
- ./src:/var/www/html:ro
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
db:
image: mysql:8.4
environment:
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- dbdata:/var/lib/mysql
ports:
- "33060:3306" # доступ к базе с хоста, например из MySQL Workbench
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
retries: 10
volumes:
dbdata:
# .env рядом с compose.yaml
DB_NAME=app
DB_USER=app
DB_PASSWORD=app_local_pass
DB_ROOT_PASSWORD=root_local_pass

Что здесь важно:
mysql:8.4это LTS-ветка MySQL с долгой поддержкой. Если на сервере MariaDB, замените образ наmariadb:11.4и переменные наMARIADB_*.- Именованный том
dbdataхранит базу между перезапусками. Без него данные пропадут при удалении контейнера. condition: service_healthyзаставляет PHP ждать, пока MySQL действительно начнёт принимать подключения, а не просто запустится.- Порт
8080:80выбран, чтобы не конфликтовать с XAMPP или IIS на порту 80. Порт базы снаружи33060по той же причине.
Запуск и основные команды docker compose
- Создайте файл
src/public/index.phpс проверкой подключения (код ниже). - В папке с
compose.yamlвыполнитеdocker compose up -d --build. - Убедитесь, что все три контейнера запущены:
docker compose ps. - Откройте в браузере
http://localhost:8080.

docker compose up -d --build # собрать образы и запустить в фоне
docker compose ps # статус контейнеров
docker compose logs -f app # логи PHP-FPM в реальном времени
docker compose exec app bash # войти в контейнер PHP
docker compose exec app php -m # список включённых расширений
docker compose restart web # перезапустить Nginx после правки конфига
docker compose down # остановить и удалить контейнеры (база в томе сохранится)
docker compose down -v # то же самое и удалить тома, база будет стёрта
Проверка подключения к MySQL из PHP
<?php
// src/public/index.php
declare(strict_types=1);
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', getenv('DB_HOST'), getenv('DB_NAME'));
try {
$pdo = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASSWORD'), [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$version = $pdo->query('SELECT VERSION()')->fetchColumn();
echo 'PHP ' . PHP_VERSION . ', MySQL ' . htmlspecialchars((string) $version);
} catch (PDOException $e) {
http_response_code(500);
echo 'Нет подключения к базе: ' . htmlspecialchars($e->getMessage());
}
Если на странице видны версии PHP и MySQL, окружение работает. Обратите внимание: хост базы берётся из переменной DB_HOST=db, то есть по имени сервиса. Больше о подключении, подготовленных запросах и обработке ошибок в статье про PDO в PHP.
Composer в контейнере
Composer уже скопирован в образ строкой COPY --from=composer:2, поэтому ставить его на свой компьютер не обязательно. Все команды выполняйте внутри контейнера, тогда зависимости соберутся под ту же версию PHP и те же расширения, что и на сайте:
docker compose exec app composer install
docker compose exec app composer require monolog/monolog
# Новый проект Laravel в пустую папку src
docker compose exec app composer create-project laravel/laravel .
Если Composer нужен один раз, а своего образа ещё нет, можно запустить официальный: docker run --rm -v "$PWD/src":/app composer:2 install. Ключ --ignore-platform-reqs лучше не использовать: он прячет проблемы с отсутствующими расширениями до момента запуска на сервере.
Права на файлы в Linux
PHP-FPM в контейнере работает от пользователя www-data (UID 33), а файлы, созданные через composer от root внутри контейнера, на хосте окажутся принадлежащими root. Чтобы этого не было, запускайте команды от своего UID: docker compose exec -u "$(id -u):$(id -g)" app composer install. Папкам, куда пишет сам сайт (кеш, загрузки, storage в Laravel), выдайте права на запись пользователю www-data.
phpMyAdmin в том же compose
Для работы с базой через браузер добавьте в services ещё один сервис:
pma:
image: phpmyadmin:latest
ports:
- "8081:80"
environment:
PMA_HOST: db
depends_on:
- db
После docker compose up -d откройте http://localhost:8081 и войдите пользователем из .env. Подробнее об интерфейсе и входе в статье про phpMyAdmin. Дамп существующей базы удобно загрузить и без него: docker compose exec -T db mysql -u root -p"$DB_ROOT_PASSWORD" app < dump.sql.
Частые ошибки
| Ошибка | Причина | Как исправить |
|---|---|---|
| 502 Bad Gateway | Nginx не достучался до PHP-FPM | Проверить fastcgi_pass app:9000 и что контейнер app запущен |
| File not found / пустая страница | Путь к коду у Nginx и PHP разный | Смонтировать ./src в одну и ту же папку в обоих |
SQLSTATE[HY000] [2002] Connection refused | В DSN указан localhost или база ещё стартует | Хост db, добавить healthcheck |
could not find driver | Не установлено расширение pdo_mysql | Добавить в docker-php-ext-install и пересобрать |
port is already allocated | Порт занят XAMPP, Open Server или другим контейнером | Сменить левую часть в ports или остановить программу |
| Изменения в Dockerfile не видны | Образ не пересобран | docker compose up -d --build |
Если раньше вы работали в XAMPP или Open Server Panel, остановите их встроенные Apache и MySQL перед запуском контейнеров или оставьте порты из примера, тогда обе среды уживутся.
Cron и фоновые задачи
В контейнере PHP-FPM служба cron не запущена. Проще всего вызывать задания с хоста: строка в системном crontab вида */5 * * * * cd /home/user/myapp && docker compose exec -T app php cron/task.php. Для Laravel рядом добавляют отдельный сервис на том же образе с командой php artisan schedule:work или php artisan queue:work. Синтаксис расписания и отладка заданий разобраны в статье про cron в Linux.
Частые вопросы
Какой образ PHP выбрать для Docker?
Для связки с Nginx берите php:8.5-fpm или php:8.4-fpm под версию вашего сервера. Образ с Apache (php:8.5-apache) нужен проектам, которые опираются на .htaccess.
Как установить расширение PHP в Docker?
Добавьте в Dockerfile строку RUN docker-php-ext-install pdo_mysql intl, предварительно поставив нужные -dev-библиотеки через apt, и пересоберите образ командой docker compose up -d --build. Расширения из PECL ставятся через pecl install и docker-php-ext-enable.
Почему PHP в Docker не подключается к MySQL по localhost?
Внутри контейнера localhost это сам контейнер PHP. Используйте имя сервиса базы из compose.yaml, например host=db.
Как запустить Composer в контейнере?
Скопируйте его в образ строкой COPY --from=composer:2 /usr/bin/composer /usr/bin/composer и выполняйте docker compose exec app composer install.
Что значит 502 Bad Gateway в связке Nginx и PHP-FPM?
Nginx не получил ответ от PHP-FPM. Проверьте, что контейнер app запущен (docker compose ps), что в fastcgi_pass указано app:9000, и посмотрите docker compose logs app.



