Deploying WordPress on a domain level is straightforward and easy, but deployment in a subdirectory is a bit complex.
This guide walks you through deploying WordPress in a subdirectory (e.g., /blog) using Docker Compose, following best practices for a production-grade WordPress stack. We’ll use the official WordPress image, MariaDB, Valkey (Redis alternative), and Caddy for reverse proxying. All configuration is based on the provided workspace files.
At the time of writing, the latest version of WordPress is 6.8.3.
The goals of this how it to create High Performance, Sectured, Dockeried, HTTPS with Let’s Encrypt, Object Caching and High Maintainability wordpress stack.
Architecture ➥
Internet → Caddy → [ Valkey → WordPress → MariaDB ]
- Caddy acts as your reverse proxy
- Valkey (Redis-compatible) caching layer
- WordPress communicates with Valkey for object caching
- MariaDB for the database
Note: The reverse proxy is not included in the stack, but the Caddy configuration provided assumes Caddy is running on the host machine.
In this setup, we are using WordPress CLI (optional). It is usefull in automating the configuration of WordPress.
sh → WordPress CLI → MariaDB
For this setup, we will need the following files:
wordpress_docker/
├── .env # .env to store credentials
├── docker-compose.yml # container orchestration
└── htaccess # htaccess to configure wordpress
Implementation ➥
mkdir wordpress_docker
cd wordpress_docker
touch .env docker-compose.yml htaccess
file: .env ➥
DB_ROOT_PASSWORD=wordpressStrongRootPassword
DB_NAME=wordpress
DB_USER=wordpress
DB_PASSWORD=wordpressStrongDBPassword
Edit the .env file and add the content below. This ensures your database credentials are secure and easily configurable. Make sure to choose a strong password for DB_ROOT_PASSWORD and DB_PASSWORD.
file: htaccess ➥
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
Important: The
htaccessfile is required for WordPress to function correctly. WordPress will override thehtaccessfile with its own configuration, but we don’t want that, so we are mounting thehtaccessfilereadonlyto the/var/www/html/.htaccesslater in thedocker-compose.yml.
file: docker-compose.yml ➥
services:
wordpress_db:
image: mariadb:lts
container_name: wordpress_db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- ./data/db:/var/lib/mysql
networks:
- wordpress_internal
wordpress_cache:
image: valkey/valkey:9.0.0-alpine # official valkey image latest
container_name: wordpress_cache
restart: unless-stopped
command: valkey-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- ./data/valkey:/data
networks:
- wordpress_internal
wordpress:
image: wordpress:latest # official wordpress image latest
container_name: wordpress
restart: unless-stopped
depends_on:
- wordpress_db
- wordpress_cache
ports:
- "8080:80" # WordPress → Host port 8080
environment:
WORDPRESS_DB_HOST: wordpress_db:3306
WORDPRESS_DB_NAME: ${DB_NAME}
WORDPRESS_DB_USER: ${DB_USER}
WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
WORDPRESS_CONFIG_EXTRA: |
define('WP_MAX_MEMORY_LIMIT', '256M' );
define('WP_REDIS_HOST', 'wordpress_cache');
define('WP_REDIS_PORT', 6379);
define('WP_CACHE', true);
if (!defined('WP_DEBUG')) {
define('WP_DEBUG', true);
}
define('WP_DEBUG_DISPLAY', false );
define('WP_DISABLE_FATAL_ERROR_HANDLER', false );
define('WP_DEBUG_LOG', '/var/www/html/wp-debug.log' );
define('WP_ENVIRONMENT_TYPE', 'production' );
define('CONCATENATE_SCRIPTS', true );
define('DISABLE_WP_CRON', true);
define('RECOVERY_MODE_EMAIL','admin@domain.com');
define('WP_SITEURL', 'https://domain.com/blog' );
define('WP_HOME', 'https://domain.com/blog' );
// FORCE HTTPS
if (isset($$_SERVER['HTTP_X_FORWARDED_PROTO']) && $$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$$_SERVER['HTTPS'] = 'on';
}
// RESTORE SUBDIRECTORY PATH (SMARTER VERSION)
// If REQUEST_URI is missing '/blog' at the start, add it back.
if (isset($$_SERVER['REQUEST_URI']) && strpos($$_SERVER['REQUEST_URI'], '/blog') !== 0) {
$$_SERVER['REQUEST_URI'] = '/blog' . $$_SERVER['REQUEST_URI'];
}
volumes:
- ./data/html:/var/www/html
- ./data/themes:/var/www/html/wp-content/themes
- ./data/plugins:/var/www/html/wp-content/plugins
- ./data/wc-logs:/var/www/html/wp-content/uploads/wc-logs
- ./htaccess:/var/www/html/.htaccess:ro
- ./data/wp-debug.log:/var/www/html/wp-debug.log
networks:
- proxy
- wordpress_internal
wordpress_cli:
image: wordpress:cli # official wordpress cli image latest
container_name: wordpress_cli
depends_on:
- wordpress_db
- wordpress
environment:
WORDPRESS_DB_HOST: wordpress_db:3306
WORDPRESS_DB_NAME: ${DB_NAME}
WORDPRESS_DB_USER: ${DB_USER}
WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
volumes:
- ./data/html:/var/www/html
- ./data/themes:/var/www/html/wp-content/themes
- ./data/plugins:/var/www/html/wp-content/plugins
networks:
- wordpress_internal
command: tail -f /dev/null
networks:
wordpress_internal:
driver: bridge
Note: The WORDPRESS_CONFIG_EXTRA block customizes WordPress for the /blog subdirectory and enables Redis caching.
file: /etc/caddy/Caddyfile ➥
domain.com {
# Enable gzip compression
encode gzip
# Client caching response headers (applies to all responses)
header {
Cache-Control "public, max-age=86400"
X-Cache-Enabled "true"
X-Cache-Disabled "false"
X-Srcache-Store-Status "BYPASS"
X-Srcache-Fetch-Status "BYPASS"
}
# WordPress blog section (including all WordPress paths under /blog)
handle_path /blog/* {
reverse_proxy wordpress:80
}
# Redirect /blog to /blog/
redir /blog /blog/ permanent
# Default: serve static files for everything else
handle {
root * /usr/share/caddy
file_server
}
}
This is for the Caddy reverse proxy running on the host machine.
If you use Nginx or any other reverse proxy, you need to configure it accordingly.
Add the above route to the Caddy reverse proxy:
This ensures:
- All
/blog/* requests go to WordPress /blogredirects to `/blog/** for consistency- Other paths serve static files, for example, we show the default Caddy page
Start the Stack ➥
cd wordpress_docker
sudo docker compose up -d
This will start all services in the background. You can check status with: docker compose ps
Visit domain.com/blog or domain.com/blog/wp-admin to complete the installation of WordPress.
For Object Caching to work, you need to install and enable the Redis Object Cache plugin in WordPress.
By following this guide, you can run WordPress securely in a subdirectory using Docker and Caddy. This setup is ideal for multi-app environments or when you want WordPress to power only a section of your site.