Docker Compose для PHP: собираем Nginx + PHP-FPM + MySQL за 10 минут

Каждый 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 одновременно.

Архитектура Docker Compose: Nginx проксирует запросы к PHP-FPM, PHP-FPM обращается к MySQL
Схема взаимодействия контейнеров в Docker Compose

Структура проекта

Создайте следующую структуру каталогов. Именно она будет использоваться во всех примерах далее:

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 с пустыми значениями для команды.

Чеклист для старта

  1. Убедитесь, что Docker Desktop (или Docker Engine + Compose plugin) установлен: docker compose version
  2. Создайте структуру проекта с 4 файлами: docker-compose.yml, Dockerfile, default.conf, index.php
  3. Запустите docker compose up -d --build
  4. Проверьте http://localhost — должны отобразиться версии PHP и MySQL
  5. Добавьте в .gitignore: .env, db_data/, vendor/
  6. Для продакшна: отключите Xdebug, display_errors и включите opcache.validate_timestamps=0

Это окружение подходит для любого PHP-фреймворка — Laravel, Symfony, Yii, WordPress. Достаточно положить код в src/ и при необходимости поправить root в конфиге Nginx (например, на /var/www/html/public для Laravel).