WordPress Docker: Slow Performance — Fix

The Problem

WordPress runs noticeably slower in Docker than on bare metal. Pages take 2-5+ seconds to load. The admin dashboard feels sluggish. TTFB (Time to First Byte) exceeds 1 second even on fast hardware.

This is one of the most common complaints about self-hosted WordPress with Docker, and it has several fixable causes.

The Causes

WordPress-in-Docker performance issues usually come from one or more of these:

  1. Slow volume mounts — Docker’s file system layer adds overhead, especially on macOS/Windows
  2. Untuned PHP-FPM — Default worker counts are too low for WordPress
  3. Missing OPcache — PHP recompiles every file on every request
  4. Unoptimized MySQL/MariaDB — Default database settings aren’t tuned for WordPress
  5. No page caching — Every request hits PHP and the database
  6. Missing Redis/Memcached — Object cache isn’t configured

The Fix

Method 1: Enable OPcache (Biggest Impact)

OPcache pre-compiles PHP files so they don’t need to be parsed on every request. This alone can cut response times by 50-70%.

Create a php-custom.ini file:

; OPcache settings
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.revalidate_freq=60
opcache.fast_shutdown=1
opcache.enable_cli=0

; PHP performance
upload_max_filesize=64M
post_max_size=64M
memory_limit=256M
max_execution_time=300
max_input_time=300

Mount it in your Docker Compose:

services:
  wordpress:
    image: wordpress:6.7-php8.3-fpm
    volumes:
      - wordpress_data:/var/www/html
      - ./php-custom.ini:/usr/local/etc/php/conf.d/custom.ini:ro
    restart: unless-stopped

Method 2: Tune MySQL/MariaDB

Default MySQL settings allocate minimal memory. Add these to a custom config:

Create mysql-custom.cnf:

[mysqld]
innodb_buffer_pool_size = 256M
innodb_log_file_size = 64M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
query_cache_type = 1
query_cache_size = 32M
query_cache_limit = 2M
tmp_table_size = 64M
max_heap_table_size = 64M
max_connections = 50

Mount it in your database service:

services:
  wordpress-db:
    image: mariadb:11.4
    volumes:
      - db_data:/var/lib/mysql
      - ./mysql-custom.cnf:/etc/mysql/conf.d/custom.cnf:ro
    environment:
      MARIADB_DATABASE: wordpress
      MARIADB_USER: wordpress
      MARIADB_PASSWORD: change-this-password
      MARIADB_ROOT_PASSWORD: change-this-root-password
    restart: unless-stopped

Method 3: Add Redis Object Cache

WordPress queries the database for the same data repeatedly. Redis caches these queries in memory.

Add Redis to your Docker Compose:

services:
  wordpress-redis:
    image: redis:7.4-alpine
    container_name: wordpress-redis
    restart: unless-stopped
    volumes:
      - redis_data:/data

Then install the Redis Object Cache plugin in WordPress:

  1. Install “Redis Object Cache” by Till Krüss from the WordPress plugin directory
  2. Add to wp-config.php (or mount via volume):
define('WP_REDIS_HOST', 'wordpress-redis');
define('WP_REDIS_PORT', 6379);
  1. Activate the plugin and enable Redis from Settings → Redis

Method 4: Use PHP-FPM Instead of Apache

The default wordpress:latest image uses Apache with mod_php. The FPM variant is significantly faster:

services:
  wordpress:
    image: wordpress:6.7-php8.3-fpm  # FPM variant, not apache
    volumes:
      - wordpress_data:/var/www/html
    environment:
      WORDPRESS_DB_HOST: wordpress-db
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: change-this-password
    restart: unless-stopped

  wordpress-nginx:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"
    volumes:
      - wordpress_data:/var/www/html:ro
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - wordpress
    restart: unless-stopped

Create nginx.conf:

server {
    listen 80;
    server_name _;
    root /var/www/html;
    index index.php;

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

    location ~ \.php$ {
        fastcgi_pass wordpress:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
    }

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

Method 5: Fix Volume Performance (macOS/Windows)

On macOS and Windows, Docker volumes are significantly slower than on Linux due to file system translation. If you’re developing locally:

services:
  wordpress:
    image: wordpress:6.7-php8.3-fpm
    volumes:
      - wordpress_data:/var/www/html  # Use named volume, not bind mount

Named volumes perform better than bind mounts (./wordpress:/var/www/html) on non-Linux hosts. On Linux, there’s no difference.

Optimized Complete Stack

Here’s a full Docker Compose with all optimizations applied:

services:
  wordpress:
    image: wordpress:6.7-php8.3-fpm
    volumes:
      - wordpress_data:/var/www/html
      - ./php-custom.ini:/usr/local/etc/php/conf.d/custom.ini:ro
    environment:
      WORDPRESS_DB_HOST: wordpress-db
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: change-this-password
    depends_on:
      wordpress-db:
        condition: service_healthy
    restart: unless-stopped

  wordpress-nginx:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"
    volumes:
      - wordpress_data:/var/www/html:ro
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - wordpress
    restart: unless-stopped

  wordpress-db:
    image: mariadb:11.4
    volumes:
      - db_data:/var/lib/mysql
      - ./mysql-custom.cnf:/etc/mysql/conf.d/custom.cnf:ro
    environment:
      MARIADB_DATABASE: wordpress
      MARIADB_USER: wordpress
      MARIADB_PASSWORD: change-this-password
      MARIADB_ROOT_PASSWORD: change-this-root-password
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  wordpress-redis:
    image: redis:7.4-alpine
    restart: unless-stopped
    volumes:
      - redis_data:/data

volumes:
  wordpress_data:
  db_data:
  redis_data:

Prevention

  • Always use the PHP-FPM variant (wordpress:x.x-phpx.x-fpm) with Nginx, not the Apache variant
  • Mount a custom php.ini with OPcache enabled from day one
  • Tune MariaDB/MySQL memory settings for your available RAM
  • Install Redis Object Cache early — it’s easier than retrofitting later
  • Use a caching plugin (WP Super Cache or W3 Total Cache) for full-page caching
  • Monitor TTFB with browser developer tools — target under 200ms

Comments