Оптимизация Docker-образов для PHP-приложений: практическое руководство — PROG-TIME

Оптимизация Docker-образов для PHP-приложений: практическое руководство

09.04.2026

Docker прочно вошёл в стандартный стек PHP-разработчика. Но часто встречаю ситуацию: образ собирается минут 10, весит полтора гигабайта, а в продакшене контейнер стартует медленнее, чем хотелось бы. В этой статье разберём, как строить компактные, быстрые и безопасные Docker-образы для PHP-приложений.

Зачем вообще оптимизировать образы

Раздутый образ — это не просто неэстетично. Это:

  • Долгое время сборки в CI/CD пайплайне
  • Медленная доставка образа на сервер (docker pull)
  • Больший attack surface: каждый лишний пакет — потенциальная уязвимость
  • Повышенное потребление памяти на сервере

Оптимизация образа напрямую влияет на скорость деплоя и стоимость инфраструктуры.

Выбор базового образа

Первый и самый важный шаг. Многие начинают с:

FROM php:8.3-apache

Это удобно, но образ весит ~500 MB. Для большинства задач лучше взять:

FROM php:8.3-fpm-alpine

Alpine Linux — дистрибутив размером около 5 MB. PHP-образ на его базе весит ~80–100 MB до установки расширений.

Сравнение базовых образов PHP 8.3

ОбразРазмерИспользование
php:8.3~490 MBРазработка, дебаггинг
php:8.3-apache~520 MBПростые проекты с Apache
php:8.3-fpm~460 MBProduction с Nginx (Debian)
php:8.3-fpm-alpine~95 MBProduction (рекомендуется)
php:8.3-cli-alpine~85 MBCLI-скрипты, очереди

Многоступенчатая сборка (Multi-stage build)

Это главный инструмент оптимизации. Суть: используем несколько FROM в одном Dockerfile. Финальный образ содержит только то, что нужно в runtime.

Пример для Laravel-приложения

# ============================================================
# Stage 1: Установка зависимостей Composer
# ============================================================
FROM composer:2.7 AS composer-deps

WORKDIR /app

# Копируем только файлы, нужные для установки зависимостей
COPY composer.json composer.lock ./

# Устанавливаем без dev-зависимостей, без скриптов
RUN composer install \
    --no-dev \
    --no-scripts \
    --no-autoloader \
    --prefer-dist

# Копируем исходники и генерируем автолоадер
COPY . .
RUN composer dump-autoload --optimize --no-dev

# ============================================================
# Stage 2: Сборка фронтенда (если нужно)
# ============================================================
FROM node:20-alpine AS frontend-build

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

COPY resources/ ./resources/
COPY vite.config.js ./
RUN npm run build

# ============================================================
# Stage 3: Финальный production-образ
# ============================================================
FROM php:8.3-fpm-alpine AS production

# Устанавливаем только нужные системные зависимости
RUN apk add --no-cache \
    libpng-dev \
    libjpeg-turbo-dev \
    libwebp-dev \
    libzip-dev \
    icu-dev

# Устанавливаем PHP-расширения
RUN docker-php-ext-configure gd \
        --with-jpeg \
        --with-webp && \
    docker-php-ext-install -j$(nproc) \
        gd \
        pdo_mysql \
        zip \
        intl \
        opcache

# Копируем только артефакты из предыдущих стадий
COPY --from=composer-deps /app/vendor ./vendor
COPY --from=composer-deps /app .
COPY --from=frontend-build /app/public/build ./public/build

# Конфигурация OPcache
COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini

# Запускаем как непривилегированный пользователь
RUN addgroup -g 1000 appgroup && \
    adduser -u 1000 -G appgroup -s /bin/sh -D appuser

USER appuser

EXPOSE 9000
CMD ["php-fpm"]

Итог: финальный образ не содержит Composer, Node.js, исходники npm — только то, что нужно для работы приложения.

OPcache: обязательная настройка для production

PHP без OPcache в продакшене — распространённая ошибка. OPcache компилирует PHP-код в байт-код и кеширует его в памяти.

Файл docker/php/opcache.ini:

[opcache]
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0   ; В production НЕ проверяем изменения файлов
opcache.revalidate_freq=0
opcache.fast_shutdown=1
opcache.jit=1255                ; JIT (PHP 8+)
opcache.jit_buffer_size=100M

Важно: validate_timestamps=0 означает, что изменения файлов не будут подхватываться без перезапуска контейнера. Это нормально для production, но неудобно при разработке.

Кеширование слоёв Docker

Docker кеширует каждый слой. Порядок инструкций критически важен: слои, которые меняются редко, должны идти раньше.

Плохо — кеш инвалидируется при любом изменении кода

COPY . .
RUN composer install

Хорошо — кеш инвалидируется только при изменении composer.json/lock

COPY composer.json composer.lock ./
RUN composer install
COPY . .

Логика проста: composer.json меняется значительно реже, чем исходный код. Выносим установку зависимостей перед копированием всего проекта.

.dockerignore: что не должно попасть в контекст сборки

Без .dockerignore Docker отправляет в контекст сборки всё — включая node_modules, .git, кеши. Это замедляет сборку и увеличивает образ.

Минимальный .dockerignore для PHP-проекта:

.git
.gitignore
node_modules
vendor
.env
.env.*
*.log
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
tests
docs
docker/dev
README.md
.phpunit.cache

Частые ошибки

1. Устанавливаете dev-зависимости в production-образ

# Неправильно
composer install

# Правильно
composer install --no-dev --optimize-autoloader

Dev-зависимости (PHPUnit, PHPStan, Faker и т.д.) не нужны в production и увеличивают образ на десятки мегабайт.

2. Запускаете контейнер от root

Контейнер, работающий от root, при компрометации даёт злоумышленнику root-права на хосте (в некоторых конфигурациях). Всегда создавайте отдельного пользователя:

RUN addgroup -g 1000 appgroup && \
    adduser -u 1000 -G appgroup -s /bin/sh -D appuser
USER appuser

3. Объединяете все RUN в один слой без причины

Иногда советуют объединять все RUN в одну команду. Это снижает количество слоёв, но убивает кеш при любом изменении. Объединяйте только логически связанные команды:

# Хорошо: установка и очистка в одном слое (чтобы не сохранять кеш apk)
RUN apk add --no-cache --virtual .build-deps \
        gcc \
        make && \
    docker-php-ext-install pdo_mysql && \
    apk del .build-deps

4. Копируете .env в образ

Файл .env содержит секреты. Он не должен попадать в образ. Передавайте переменные окружения через docker-compose.yml или систему оркестрации:

services:
  app:
    environment:
      DB_HOST: db
      DB_PASSWORD: ${DB_PASSWORD}  # Из .env на хосте

5. Не указываете версии пакетов

# Плохо: сборка недетерминирована
RUN apk add libpng-dev

# Лучше: фиксируем версию
RUN apk add libpng-dev=1.6.43-r0

Без фиксации версий сборка сегодня и через месяц может дать разные результаты.

Практические советы

Используйте --no-cache при установке пакетов Alpine:

RUN apk add --no-cache libzip-dev

Без этого кеш apk сохраняется в слое и увеличивает образ.

Проверяйте размер слоёв:

docker history my-php-app:latest

Это покажет, какой слой сколько весит и где спрятаны лишние мегабайты.

Используйте dive для анализа образа:

# Установка
brew install dive

# Анализ
dive my-php-app:latest

dive показывает содержимое каждого слоя и помогает найти лишние файлы.

Разделяйте образы для dev и production:
В разработке удобно иметь Xdebug, отладочные инструменты, composer внутри контейнера. Используйте target для сборки нужной стадии:

# Dev-образ (со всеми инструментами)
docker build --target development -t myapp:dev .

# Production-образ
docker build --target production -t myapp:prod .

FAQ

Q: Нужно ли использовать Alpine для всех PHP-проектов?

Не обязательно. Alpine основан на musl libc вместо glibc, что иногда вызывает проблемы с отдельными расширениями (особенно старыми). Если возникают сложности — берите php:8.3-fpm-slim (Debian Slim): компромисс между размером и совместимостью.

Q: Стоит ли включать JIT в PHP 8?

Для типичных веб-приложений JIT даёт минимальный прирост (иногда даже замедляет). JIT эффективен для вычислительно-интенсивных задач: рендеринг, обработка данных, математика. Для CRUD-приложений — скорее маркетинг.

Q: Как обновлять образ безопасно?

Используйте теги с версиями (php:8.3.10-fpm-alpine), а не latest. Обновляйте образы регулярно (еженедельно) для получения security-патчей. Автоматизируйте это через Dependabot или Renovate.

Q: Нужен ли supervisor внутри контейнера?

Нет, если вы следуете принципу «один процесс на контейнер». Очереди, cron, php-fpm — отдельные контейнеры. Управление процессами — задача оркестратора (Docker Compose, Kubernetes), а не supervisor.

Q: Как ускорить сборку в CI/CD?

Используйте BuildKit и кеш между сборками:

# GitHub Actions
- uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

Вывод

Оптимизация Docker-образов для PHP — не разовая акция, а часть культуры разработки. Ключевые принципы:

  1. Alpine или Slim вместо полного Debian-образа
  2. Multi-stage build — отделяем сборку от runtime
  3. Порядок слоёв — редко меняемое вперёд
  4. OPcache — обязателен в production
  5. Непривилегированный пользователь — всегда
  6. .dockerignore — не забывайте про него

Грамотно собранный образ для Laravel-приложения весит 150–200 MB вместо 600–800 MB и собирается повторно за 30 секунд вместо 5 минут.

Инвестиция небольшая, а выигрыш ощутим с первого же деплоя.