Блог

Инфраструктура 8 мин чтения

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

Docker позволяет PHP-приложению работать одинаково на ноутбуке, в тестировании и в продакшне и превращает релизы и откаты из стресса в рутину. Но он добавляет подвижных частей, а небрежная настройка хуже обычного сервера. Здесь — готовая к продакшну базовая конфигурация с реальными настройками и случаи, когда он не нужен.

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

Коротко

  • Docker стоит использовать, когда нужны одинаковые окружения, быстрые откаты и простой вход в проект. Для одного небольшого сайта-визитки на виртуальном хостинге он обычно лишний.
  • Собирайте неизменяемый образ в CI: многоэтапная сборка, только продакшн-зависимости, пользователь без root, включённый OPcache, ошибки пишутся в лог и никогда не показываются.
  • Выпускайте релизы по тегу образа (хеш коммита Git), проверяйте здоровье после старта и автоматически откатывайтесь на предыдущий тег, если проверка не прошла.
  • Контейнеры одноразовые, данные — нет: держите базу и загрузки в томах или управляемых сервисах и делайте бэкапы вне Docker.
  • Самые частые ошибки: секреты в образе, опубликованные порты базы, неограниченные логи и лимиты php-fpm, не соответствующие имеющейся памяти.

Для многих бизнес-приложений главная стоимость хостинга — не сервер, а люди. Настройка, которую понимает только один разработчик, деплой руками через FTP и тестовое окружение, отличающееся от продакшна, в долгосрочной перспективе обходятся дорого. Контейнеры решают это, упаковывая приложение вместе с точной версией PHP и расширениями. Мы сами запускаем свои PHP-сайты и клиентские системы на стеке с Docker, и это руководство описывает базу, с которой мы начинаем.

Когда Docker нужен, а когда нет

  • Нужен: несколько сервисов (веб, PHP, база, очередь), больше одного разработчика, нужны повторяемые релизы и быстрые откаты или планируется переезд между провайдерами.
  • Не обязателен: простой контентный сайт, который редко меняется, на управляемом хостинге. Дополнительная сложность должна окупаться.
  • Не заменяет нормальную эксплуатацию: по-прежнему нужны обновления, бэкапы и мониторинг. См. наше руководство по бэкапам и мониторингу.

Целевая архитектура

Надёжная и простая схема для PHP-приложения на одном сервере:

  • Обратный прокси и TLS на границе (Nginx или Caddy на хосте либо CDN вроде Cloudflare, см. наше руководство по Cloudflare).
  • Веб-контейнер (Nginx), который отдаёт статику и передаёт PHP-запросы дальше.
  • Контейнер PHP-FPM с вашим кодом, встроенным в образ.
  • База данных в отдельном контейнере с постоянным томом или, лучше, управляемый сервис базы для критичных данных.
  • Тома только для данных, которые должны пережить пересоздание: файлы базы и загрузки пользователей.

Многоэтапный Dockerfile

Многоэтапная сборка устанавливает зависимости на одном этапе и копирует только результат в компактный рабочий образ. В итоговом образе нет Composer, компиляторов и тестовых инструментов, поэтому он меньше, а у атакующего меньше возможностей:

# ---- этап 1: зависимости PHP ----
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-interaction --optimize-autoloader

# ---- этап 2: рабочий образ ----
FROM php:8.3-fpm-alpine

RUN apk add --no-cache icu-libs libzip \
 && apk add --no-cache --virtual .build icu-dev libzip-dev $PHPIZE_DEPS \
 && docker-php-ext-install -j"$(nproc)" pdo_mysql intl zip opcache \
 && apk del .build

COPY docker/php.ini  /usr/local/etc/php/conf.d/zz-app.ini
COPY docker/www.conf /usr/local/etc/php-fpm.d/zz-www.conf

WORKDIR /var/www/app
COPY --from=vendor /app/vendor ./vendor
COPY --chown=www-data:www-data . .

USER www-data
EXPOSE 9000

Выбирайте версию PHP осознанно и проверяйте, что она ещё поддерживается. Как это спланировать, объясняет наше руководство по обновлению до PHP 8. Фиксируйте версии базовых образов и обновляйте их по расписанию: образ, который никогда не пересобирают, накапливает известные уязвимости. Держите .dockerignore, исключающий .git, .env, тесты и локальные данные, чтобы ничего чувствительного не попало в образ.

Продакшн-конфигурация PHP и php-fpm

# docker/php.ini
expose_php = 0
display_errors = 0
log_errors = 1
memory_limit = 256M
upload_max_filesize = 10M
post_max_size = 12M

opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0   ; код в образе не меняется, пока образ работает

# docker/www.conf
[www]
pm = dynamic
pm.max_children = 12
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
request_terminate_timeout = 30s
catch_workers_output = yes
decorate_workers_output = no

Параметр opcache.validate_timestamps = 0 даёт лучшую производительность для неизменяемого образа, но работает, только если вы никогда не правите файлы в работающем контейнере: вместо этого выпускается новый образ. Рассчитывайте пул по реальным цифрам: измерьте среднюю память одного процесса PHP под нагрузкой, решите, сколько памяти сервера может занимать PHP, и разделите. Если процесс занимает 60 МБ, а PHP можно отдать 1 ГБ, pm.max_children — около 16. Намного большее значение приводит к свопу или к тому, что ядро убивает процессы при всплеске трафика. Слишком малое — запросы встают в очередь. Следите в логе php-fpm за предупреждениями «max_children reached».

Compose-файл с правильными значениями по умолчанию

x-logging: &default-logging
  driver: json-file
  options: { max-size: "10m", max-file: "5" }     # логи не заполнят диск

services:
  web:
    image: ghcr.io/your-org/app-web:${TAG}
    ports: ["127.0.0.1:8080:80"]                  # достучаться может только локальный обратный прокси
    depends_on: [php]
    restart: unless-stopped
    logging: *default-logging
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1/health.php"]
      interval: 15s
      timeout: 3s
      retries: 3

  php:
    image: ghcr.io/your-org/app:${TAG}
    env_file: .env                                 # секреты остаются на сервере, не в образе
    restart: unless-stopped
    read_only: true
    tmpfs: [/tmp]
    mem_limit: 512m
    volumes: [uploads:/var/www/app/storage/uploads]
    logging: *default-logging

  db:
    image: mysql:8.4
    env_file: .env.db
    restart: unless-stopped
    volumes: [db_data:/var/lib/mysql]              # нет "ports:" - база не публична
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
      interval: 15s
      retries: 5
    logging: *default-logging

volumes:
  uploads:
  db_data:

Образ web — небольшой образ Nginx, в который скопированы папка public/ и конфигурация сайта; он собирается в том же конвейере, что и образ PHP. Главное: привязывайте публикуемые порты к 127.0.0.1, никогда не публикуйте базу, задавайте политики перезапуска, ограничивайте память, чтобы один контейнер не положил хост, и ограничивайте размер логов. Помните, что порты, опубликованные Docker, могут обходить правила файервола хоста, как мы объясняем в руководстве по Cloudflare. Никогда не монтируйте сокет Docker в контейнер приложения: это даёт контейнеру контроль над хостом.

Релизы и откаты

Лучшая особенность схемы на образах в том, что каждый релиз — это тег, а откат — «запусти предыдущий тег». Помечайте образы хешем коммита Git, не полагайтесь на latest и добавьте в скрипт релиза проверку здоровья:

#!/usr/bin/env bash
# /srv/app/release.sh <git-sha>
set -euo pipefail
TAG="${1:?usage: release.sh <git-sha>}"
cd /srv/app
PREV="$(cat .current_tag 2>/dev/null || true)"

export TAG
docker compose pull php web
docker compose up -d --no-deps php web

for i in $(seq 1 30); do
  if curl -fsS http://127.0.0.1:8080/health.php > /dev/null; then
    echo "$TAG" > .current_tag
    echo "healthy: $TAG"; exit 0
  fi
  sleep 2
done

echo "unhealthy after release, rolling back to ${PREV:-none}" >&2
if [ -n "$PREV" ]; then TAG="$PREV" docker compose up -d --no-deps php web; fi
exit 1

Изменения базы требуют аккуратности: пишите миграции так, чтобы предыдущая версия кода работала с новой схемой (сначала добавляйте колонки, удаляйте их в следующем релизе). Именно это делает быстрый откат безопасным. Перед любым релизом, меняющим схему, делайте бэкап базы.

Сборка и доставка из CI

Собирайте образ один раз в CI и разворачивайте именно этот артефакт. Минимальный workflow GitHub Actions:

name: build-and-release
on:
  push:
    branches: [main]
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/your-org/app:${{ github.sha }}
      - name: Release over SSH
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: |
          install -m 600 /dev/null key && echo "$DEPLOY_KEY" > key
          ssh -i key -o StrictHostKeyChecking=accept-new deploy@your-server "/srv/app/release.sh ${{ github.sha }}"

Запускайте тесты и статический анализ в том же конвейере до сборки образа и используйте ключ деплоя, который может запускать только скрипт релиза. Добавьте сканирование образа на уязвимости, если этого требует ваш профиль рисков.

Логи, данные и безопасность

  • Логи идут в stdout и stderr, ротируются Docker, а важное отправляется в центральное место, чтобы пережить контейнер.
  • Данные живут вне контейнера. Том — это не бэкап. Используйте подход из нашего руководства по бэкапам: шифрование, копия вне площадки, проверка восстановления.
  • Секреты никогда не вшиваются в образы. Передавайте их при запуске через env-файлы или менеджер секретов и не храните в Git.
  • Запускайте от пользователя без root, по возможности используйте файловую систему только для чтения и убирайте лишнее.
  • Обновляйте образы. Пересобирайте регулярно, чтобы получать исправления безопасности базового образа и PHP.
  • Применяйте меры уровня приложения из нашего чеклиста безопасности PHP: контейнеры не исправят небезопасный код.

Чеклист готовности к продакшну

  1. Многоэтапный образ только с продакшн-зависимостями, зафиксированный базовый образ, пользователь без root.
  2. OPcache включён, ошибки пишутся в лог, а не показываются, php-fpm рассчитан по измеренной памяти.
  3. Compose-файл с политиками перезапуска, healthcheck, лимитами памяти, ротацией логов и без публичного порта базы.
  4. Релизы по тегу с хешем коммита, автоматической проверкой здоровья и откатом.
  5. Бэкапы и проверенное восстановление базы и загрузок.
  6. Мониторинг и алерты по здоровью, диску, ошибкам и сертификатам.

Чем мы можем помочь

Мы контейнеризуем PHP-приложения, настраиваем CI-конвейеры и релизы и подключаем их к платежам, CRM и мессенджерам. Посмотрите нашу услугу интеграций или запишитесь на звонок, и мы изучим ваш текущий хостинг и процесс релизов.

Частые вопросы

Подходит ли Docker для запуска PHP в продакшне?

Да, если использовать его правильно. Он даёт одинаковые окружения, повторяемые релизы и быстрые откаты. Нужна корректная настройка: неизменяемые образы, healthcheck, лимиты ресурсов, ротация логов, управление секретами и бэкапы данных вне контейнера.

Нужно ли запускать базу данных в Docker в продакшне?

Для небольших и средних систем можно, если используется постоянный том, база не открыта публично и бэкапы проверены. Для критичных данных многие команды предпочитают управляемый сервис базы, который берёт на себя отказоустойчивость, обновления и бэкапы.

Как откатить релиз в Docker?

Помечайте каждый образ хешем коммита Git и храните предыдущий тег. Для отката запустите контейнеры с предыдущим тегом. Пишите миграции базы так, чтобы старый код работал с новой схемой.

Сколько воркеров php-fpm настраивать?

Измерьте среднюю память одного воркера под нагрузкой, решите, сколько памяти может занимать PHP, и разделите. Слишком большое значение вызывает своп, слишком малое — очередь запросов. Следите в логе php-fpm за предупреждениями max_children.

Какая самая большая ошибка безопасности Docker в PHP-приложениях?

Секреты в образе или Git и порты базы, опубликованные в интернет. Также избегайте запуска от root, монтирования сокета Docker в контейнеры и необновляемого базового образа.

Бесплатный discovery-звонок

Хаотичный стек или зависшая интеграция - Integrono наметит самый умный первый шаг.

Вы получаете senior-взгляд на бюджетные ловушки, зависимости и когда покупать, склеивать или писать - до старта оплаты. CEO остаётся в контуре.