PHPFlowPHP, MySQL, Laravel и JavaScript

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 в контейнере и разбором частых ошибок.

Содержание
  1. Как устроено окружение Docker для PHP
  2. Что установить перед началом
  3. Структура проекта
  4. Dockerfile для PHP-FPM с расширениями
  5. Конфигурация Nginx для PHP-FPM
  6. Файл compose.yaml: PHP, Nginx и MySQL
  7. Запуск и основные команды docker compose
  8. Composer в контейнере
  9. phpMyAdmin в том же compose
  10. Частые ошибки
  11. 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 нужен для консольных скриптов и воркеров.

Схема связки Nginx, PHP-FPM и MySQL в Docker Compose
Путь запроса в окружении Docker: Nginx передаёт PHP-скрипты в PHP-FPM, а тот подключается к MySQL по имени сервиса

Каждый сервис живёт в своём контейнере, а Docker Compose связывает их общей сетью. Внутри этой сети контейнеры обращаются друг к другу по имени сервиса: Nginx передаёт PHP-запросы на app:9000, а PHP подключается к базе по адресу db, а не localhost. Это главное отличие от XAMPP или Open Server, где всё работает на одной машине.

ОбразДля чегоКогда выбрать
php:8.5-fpmPHP-FPM без веб-сервераСвязка с Nginx, основной вариант
php:8.5-apacheApache и 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
Dockerfile для PHP-FPM с расширениями
Сокращённый Dockerfile: базовый образ PHP-FPM, расширения через docker-php-ext-install и Composer

Как работает 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, для zip libzip-dev, для gd libpng-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
Сервисы app, web и db в compose.yaml
Три сервиса в compose.yaml: имена app, web и db служат адресами внутри сети Docker

Что здесь важно:

  • mysql:8.4 это LTS-ветка MySQL с долгой поддержкой. Если на сервере MariaDB, замените образ на mariadb:11.4 и переменные на MARIADB_*.
  • Именованный том dbdata хранит базу между перезапусками. Без него данные пропадут при удалении контейнера.
  • condition: service_healthy заставляет PHP ждать, пока MySQL действительно начнёт принимать подключения, а не просто запустится.
  • Порт 8080:80 выбран, чтобы не конфликтовать с XAMPP или IIS на порту 80. Порт базы снаружи 33060 по той же причине.

Запуск и основные команды docker compose

  1. Создайте файл src/public/index.php с проверкой подключения (код ниже).
  2. В папке с compose.yaml выполните docker compose up -d --build.
  3. Убедитесь, что все три контейнера запущены: docker compose ps.
  4. Откройте в браузере http://localhost:8080.
Вывод docker compose up и docker compose ps
Запуск окружения: Compose дожидается готовности базы и запускает PHP и Nginx
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 GatewayNginx не достучался до 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.