Официальный образ 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 до новой версии) и переносить между хостингами без потери конфигурации.