Docker Compose для WordPress: Полный стек Nginx, PHP-FPM, MariaDB и Redis

Официальный образ wordpress:latest удобен для локального тестирования, но для продакшна он не подходит: внутри работает Apache и mod_php, что потребляет слишком много памяти. Правильный подход — разделить стек на независимые контейнеры: Nginx для статики и проксирования, PHP-FPM для обработки кода, MariaDB для данных и Redis для кэширования.

В этом руководстве собирается полностью изолированный, воспроизводимый и масштабируемый стек WordPress с помощью Docker Compose. Конфигурация оптимизирована под нагрузку и легко переносится между серверами.

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

Создаем директорию для проекта и подпапки для конфигов:

mkdir -p wordpress-stack/{nginx,php,db,redis}
cd wordpress-stack

Итоговая структура файлов:

wordpress-stack/
├── docker-compose.yml
├── nginx/
│   └── default.conf
├── php/
│   ├── Dockerfile
│   └── www.conf
├── db/
│   └── my.cnf
└── redis/
    └── redis.conf

Шаг 1: Файл docker-compose.yml

Создаем docker-compose.yml в корне папки. Используем именованные тома (named volumes) для баз данных, чтобы данные не терялись при пересоздании контейнеров, и bind mounts для кода и конфигов.

services:
  nginx:
    image: nginx:1.25-alpine
    container_name: wp_nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
      - wordpress_data:/var/www/html
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - php
    networks:
      - wp_network

  php:
    build: ./php
    container_name: wp_php
    restart: unless-stopped
    volumes:
      - wordpress_data:/var/www/html
      - ./php/www.conf:/usr/local/etc/php-fpm.d/www.conf:ro
    depends_on:
      - db
      - redis
    networks:
      - wp_network

  db:
    image: mariadb:10.11
    container_name: wp_db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: root_password_here
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wp_user
      MYSQL_PASSWORD: wp_password_here
    volumes:
      - db_data:/var/lib/mysql
      - ./db/my.cnf:/etc/mysql/conf.d/custom.cnf:ro
    networks:
      - wp_network

  redis:
    image: redis:7-alpine
    container_name: wp_redis
    restart: unless-stopped
    command: redis-server /usr/local/etc/redis/redis.conf
    volumes:
      - redis_data:/data
      - ./redis/redis.conf:/usr/local/etc/redis/redis.conf:ro
    networks:
      - wp_network

volumes:
  wordpress_data:
  db_data:
  redis_data:

networks:
  wp_network:
    driver: bridge

Шаг 2: Конфигурация Nginx

Создаем файл nginx/default.conf. Nginx будет проксировать PHP-запросы в контейнер php на порт 9000.

server {
    listen 80;
    server_name custom-code.ru www.custom-code.ru;
    root /var/www/html;
    index index.php index.html;

    client_max_body_size 64M;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass php:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_index index.php;
        
        fastcgi_buffer_size 128k;
        fastcgi_buffers 256 16k;
        fastcgi_busy_buffers_size 256k;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    location ~ /\. {
        deny all;
    }
}

Параметры кэширования статики и сжатия Brotli настраиваются так же, как и на bare-metal сервере. Детальный разбор конфигов Nginx для WordPress доступен в руководстве по настройке reverse proxy.

Шаг 3: Кастомный образ PHP-FPM

Стандартный образ php:fpm не содержит нужных расширений. Создаем php/Dockerfile для установки redis, imagick и других зависимостей.

FROM php:8.2-fpm

# Установка системных зависимостей
RUN apt-get update && apt-get install -y \
    libpng-dev \
    libjpeg-dev \
    libfreetype6-dev \
    libzip-dev \
    libicu-dev \
    libmagickwand-dev \
    && rm -rf /var/lib/apt/lists/*

# Установка PHP расширений
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) gd pdo pdo_mysql zip intl opcache \
    && pecl install redis imagick \
    && docker-php-ext-enable redis imagick

# Очистка кэша
RUN apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false

Создаем файл php/www.conf для настройки пула процессов. Размер пула (pm.max_children) рассчитывается исходя из доступной RAM, формула и примеры приведены в статье про оптимизацию PHP-FPM.

[www]
user = www-data
group = www-data
listen = 9000

pm = dynamic
pm.max_children = 30
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12
pm.max_requests = 500

php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 64M
php_admin_value[max_execution_time] = 300

Шаг 4: Оптимизация MariaDB и Redis

Создаем db/my.cnf. Для MariaDB критично настроить innodb_buffer_pool_size под объем доступной памяти, иначе база будет постоянно обращаться к диску.

[mysqld]
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 1
max_connections = 50
wait_timeout = 60
max_allowed_packet = 64M
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

Создаем redis/redis.conf для ограничения памяти и стратегии вытеснения:

maxmemory 256mb
maxmemory-policy allkeys-lru
save 900 1
save 300 10
save 60 10000

Подключение Redis Object Cache ускорит запросы к БД и снизит нагрузку на контейнер db. Инструкция по интеграции с WordPress описана в материале про Redis Object Cache.

Шаг 5: Запуск и установка WordPress

Собираем и запускаем стек:

# Сборка кастомного PHP образа и запуск контейнеров
docker compose up -d --build

# Проверка логов
docker compose logs -f

Скачиваем файлы WordPress в именованный том. Для этого удобно использовать временный контейнер WP-CLI:

docker run --rm \
    --network wordpress-stack_wp_network \
    -v wordpress_data:/var/www/html \
    wordpress:cli \
    core download --locale=ru_RU

Теперь открываем https://custom-code.ru в браузере и проходим стандартный мастер установки WordPress. В поле «Хост базы данных» указываем db (имя сервиса из docker-compose.yml), а не localhost.

Шаг 6: Настройка Cron в Docker

Системный cron необходим для отложенных публикаций и фоновых задач. В Docker-окружении cron можно запустить отдельным легковесным контейнером или через хост-систему.

Вариант через хост (самый простой): добавляем задачу в crontab на сервере, которая вызывает WP-CLI внутри контейнера:

sudo crontab -e

# Запуск каждые 5 минут
*/5 * * * * docker exec wp_php php /var/www/html/wp-cron.php?doing_wp_cron >/dev/null 2>&1

В самом WordPress не забываем отключить встроенный псевдо-крон, добавив в wp-config.php:

define( 'DISABLE_WP_CRON', true );

Шаг 7: SSL через Certbot на хосте

Если Nginx работает в контейнере, Certbot удобнее запускать на хост-машине. Создаем папку для сертификатов и получаем их:

mkdir -p ./certs

# Запуск Certbot в standalone режиме (нужно временно остановить Nginx контейнер)
docker compose stop nginx
sudo certbot certonly --standalone -d custom-code.ru -d www.custom-code.ru \
    --agree-tos --email admin@custom-code.ru

# Копируем сертификаты в папку certs
sudo cp /etc/letsencrypt/live/custom-code.ru/fullchain.pem ./certs/
sudo cp /etc/letsencrypt/live/custom-code.ru/privkey.pem ./certs/
sudo chown -R 101:101 ./certs # Права для Nginx внутри Alpine

docker compose start nginx

Для автопродления добавляем в cron на хосте скрипт, который копирует обновленные сертификаты и перезапускает Nginx: docker compose restart nginx.

Решение частых проблем

502 Bad Gateway

Nginx не может достучаться до PHP. Проверяем, что контейнер php запущен (docker ps) и слушает порт 9000. Также убеждаемся, что в nginx/default.conf указано fastcgi_pass php:9000;, а не 127.0.0.1:9000.

Контейнер PHP падает с кодом 137

Это означает, что контейнер был убит системой из-за нехватки памяти (OOMKilled). Нужно либо увеличить pm.max_children в www.conf, либо поднять лимиты RAM для Docker. Разбор причин и решений этой ошибки приведен в руководстве по Docker Code 137.

Медленная работа базы данных

Если контейнер db потребляет слишком много CPU, проверяем настройки my.cnf. Часто помогает увеличение innodb_buffer_pool_size и анализ медленных запросов через slow_query_log. Детальная оптимизация параметров InnoDB описана в статье про настройку MySQL/MariaDB.

Проблемы с правами на файлы

Если WordPress не может загружать медиафайлы, меняем владельца внутри контейнера PHP:

docker exec -u root wp_php chown -R www-data:www-data /var/www/html

Итог

Сборка WordPress в Docker Compose дает полную изоляцию, версионирование инфраструктуры и возможность развернуть идентичное окружение на любом сервере за 2 минуты. Такой стек легко масштабировать, заменять компоненты (например, обновить MariaDB до новой версии) и переносить между хостингами без потери конфигурации.