Containerizing WordPress for Production: Security, Redis, and Next-Gen Images

Moving WordPress to Docker requires more than just pulling the default image. If you want a production-grade environment that handles high traffic, serves next-generation images flawlessly, and traps potential security breaches in an isolated sandbox, you need a specialized architecture.
This guide walks through building an enterprise-level WordPress Docker stack behind a global Nginx reverse proxy. We will cover implementing Redis object caching, integrating CompressX for WebP/AVIF delivery on bare metal, fixing WP-CLI, and automating dynamic Nginx logging.

1. The Architecture Strategy

Before touching configuration files, it is crucial to understand why this stack is structured the way it is:

  • MariaDB over PostgreSQL: While Postgres is an incredibly powerful database engine, WordPress core and the vast majority of its plugins are hardcoded for MySQL syntax. Using PostgreSQL requires fragile translation layers that break easily. MariaDB provides high performance while maintaining 100% native WordPress compatibility.
  • Isolated Filesystem Security: Instead of mounting the entire WordPress installation to the host machine, we only mount the wp-content directory. This traps all themes, plugins, and uploads in an isolated folder. If a plugin vulnerability is exploited, the attacker cannot rewrite core files like wp-config.php because they only exist inside the immutable container memory.
  • Bare-Metal Image Delivery: Running CompressX (or similar image optimization plugins) behind a Docker proxy can bottleneck performance. We configure our global Nginx to read the WebP/AVIF files directly from the host’s physical hard drive, bypassing Docker entirely for static media delivery.

2. The Configuration Files

To keep credentials secure and make spinning up new websites effortless, decouple your configuration from your deployment script using a .env file.

The .env File

Create this file in your project directory (e.g., /var/www/site1-files/.env):

# --- SYSTEM ENVIRONMENT CONFIGURATION ---
SITE_ID=site1
HOST_WEB_PORT=8081

# --- HOST PATHS ---
# Point directly to your host's wp-content directory for security
HOST_CONTENT_PATH=/var/www/site1-files/wp-content

# --- DATABASE CREDENTIALS ---
DB_TABLE_PREFIX=wo_
DB_ROOT_PASSWORD=your_secure_root_password
DB_NAME=your_database_name
DB_USER=your_database_user
DB_PASSWORD=your_secure_db_password

# --- REDIS CONFIGURATION ---
REDIS_PASSWORD=your_secure_redis_password
REDIS_MAX_MEMORY=256mb

The Custom Dockerfile

Because the official WordPress image lacks the command-line tools needed to run internal database checks, create a Dockerfile in the same directory to inject WP-CLI and the MariaDB client binaries:

FROM wordpress:latest

# Install MariaDB client binaries for database connection checks
RUN apt-get update && apt-get install -y mariadb-client && rm -rf /var/lib/apt/lists/*

# Download and install WP-CLI natively
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
    && chmod +x wp-cli.phar \
    && mv wp-cli.phar /usr/local/bin/wp

The docker-compose.yml File

This script ties the database, object cache, and web application together securely using an internal bridge network.

version: '3.8'

services:
  db:
    image: mariadb:10.11
    container_name: ${SITE_ID}_db
    restart: always
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_NAME}
      MYSQL_USER: ${DB_USER}
      MYSQL_PASSWORD: ${DB_PASSWORD}
    networks:
      - site-network

  redis:
    image: redis:7-alpine
    container_name: ${SITE_ID}_cache
    restart: always
    command: >
      redis-server 
      --appendonly yes 
      --requirepass ${REDIS_PASSWORD} 
      --maxmemory ${REDIS_MAX_MEMORY} 
      --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    networks:
      - site-network

  wordpress:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: ${SITE_ID}_core
    restart: always
    ports:
      - "127.0.0.1:${HOST_WEB_PORT}:80"
    depends_on:
      - db
      - redis
    volumes:
      # Mount ONLY wp-content to protect core files
      - ${HOST_CONTENT_PATH}:/var/www/html/wp-content
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: ${DB_USER}
      WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
      WORDPRESS_DB_NAME: ${DB_NAME}
      WORDPRESS_TABLE_PREFIX: ${DB_TABLE_PREFIX}
      WORDPRESS_CONFIG_EXTRA: |
        define('WP_REDIS_HOST', 'redis');
        define('WP_REDIS_PASSWORD', '${REDIS_PASSWORD}');
        if (isset($$_SERVER['HTTP_X_FORWARDED_PROTO']) && $$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
            $$_SERVER['HTTPS'] = 'on';
        }
    networks:
      - site-network

volumes:
  db_data:
    name: ${SITE_ID}_db_volume
  redis_data:
    name: ${SITE_ID}_redis_volume

networks:
  site-network:
    name: ${SITE_ID}_isolated_network
    driver: bridge

3. Global Nginx & CompressX Integration

By offloading image delivery to the global Nginx reverse proxy, your Docker container is free to spend its CPU cycles executing PHP logic instead of serving heavy static files.
Create a shared configuration snippet at /etc/nginx/snippets/wordpress.conf:

# --- AUTOMATIC DYNAMIC LOGGING ---
# Generates unique log files based on the requested domain
access_log /var/log/nginx/${host}_access.log;
error_log  /var/log/nginx/${host}_error.log warn;
open_log_file_cache max=1000 inactive=20s min_uses=2 valid=1m;

# --- COMPRESX CAPABILITY DETECTORS ---
set $ext_avif ".avif";
if ($http_accept !~* "image/avif") { set $ext_avif ""; }

set $ext_webp ".webp";
if ($http_accept !~* "image/webp") { set $ext_webp ""; }

# --- IMAGE INTERCEPTOR ---
location ~ /wp-content/(?<path>.+)\.(?<ext>jpe?g|png|gif|webp)$ {
    add_header Vary Accept;
    add_header Cache-Control "public, max-age=31536000, immutable";
    expires 365d;
    log_not_found off;

    # Prioritize AVIF, fallback to WebP, then original, then Docker
    try_files 
        /wp-content/compressx-nextgen/$path.$ext$ext_avif 
        /wp-content/compressx-nextgen/$path.$ext$ext_webp 
        /wp-content/$path.$ext 
        @docker_fallback;
}

# --- STATIC ASSETS CACHING ---
location ~* \.(css|js|ico|svg|ttf|woff2?)$ {
    expires max;
    log_not_found off;
    access_log off;
    try_files $uri @docker_fallback; 
}

# --- DEFAULT DOCKER ROUTING ---
location / {
    proxy_pass http://127.0.0.1:$wordpress_port;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

location @docker_fallback {
    proxy_pass http://127.0.0.1:$wordpress_port;
    proxy_set_header Host $host;
}

Now, your individual domain server blocks remain incredibly lean:

server {
    listen 443 ssl http2;
    server_name theswanlake.com;

    ssl_certificate /etc/letsencrypt/live/domain/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/domain/privkey.pem;

    root /var/www/site1-files;
    set $wordpress_port 8081; # Matches your .env HOST_WEB_PORT

    include snippets/wordpress.conf;
}

4. Execution & Maintenance

With your files in place, executing the environment and maintaining it is straightforward.

docker compose up -d --build


Fix Nginx logging permissions
Because dynamic logging creates files under the web user, grant Nginx ownership of the logging directory on the host:

sudo chown -R www-data:adm /var/log/nginx

Fix Redis overcommit memory
Ensure the host Linux kernel allows Redis to save background forks without crashing:

(To make this permanent, add vm.overcommit_memory=1 to /etc/sysctl.conf).

sudo sysctl vm.overcommit_memory=1

Build and launch the stack
Navigate to your project folder and spin up the containers:
docker compose up -d –build

Fix WP-CLI database checks
Newer MariaDB clients strictly enforce SSL, which causes internal Docker network connections to fail. Map a quick alias in your ~/.bashrc to bypass this for internal commands:

alias wp1="docker exec -it -u www-data site1_core wp --ssl=false"

You can now run database commands safely:

wp1 db check or wp1 cache flush



By implementing this architecture, you isolate application logic into disposable containers, secure your filesystems against tampering, and guarantee that heavy resource lifting — like image optimization and caching — is handled precisely where it is most efficient.

Use it on your own responsibility.