How to Install WordPress with Docker Compose: Full Stack Guide
Install WordPress with Docker Compose. Complete stack with MySQL 8.4, phpMyAdmin, Cloudflare SSL, automatic backups, and Redis caching. Production-ready setup guide.

This guide walks you through installing WordPress with Docker Compose, covering a complete production-ready stack: MySQL 8.4 LTS, phpMyAdmin, automated database backups, optional Redis object caching, and Cloudflare Tunnels for SSL. Everything runs in five containers managed by a single compose.yaml file on any Linux VPS or Mini PC home server.
You’ll need a Linux VPS. I recommend Hetzner for the best price-to-performance ratio in Europe, or Hostinger if you prefer NVMe storage at a budget price. If you are running WooCommerce, you can also deploy the Woo Admin product dashboard alongside WordPress in Docker for faster product management.
Here’s what you get by following this guide:
- WordPress container (pinned PHP version, persistent volume)
- MySQL 8.4 LTS database (supported until 2032)
- phpMyAdmin for database management
- Automated database backups with rotation
- Redis object cache for faster database queries
- SSL via Cloudflare Tunnels (free plan)
I’ll use
latestfor some image tags below, but you can pin exact versions if you prefer predictable deploys. The one exception is MySQL. More on that in Step 3.
WordPress Docker Compose Stack: Step-by-Step Setup
1. Prerequisites for your WordPress Docker stack
- A Linux VPS with 2GB+ RAM (4GB recommended for WordPress + Redis + phpMyAdmin)
- Docker Engine 24+ and Docker Compose V2 (the
docker composeplugin, not standalonedocker-compose) - A domain name pointed at your server (or use direct IP access)
- A Cloudflare account (free plan) for Tunnels and SSL
- Port 5010 and 5011 available (or pick your own)
Docker Compose V2 syntax
This guide uses Docker Compose V2 syntax. The standalone docker-compose (V1) is deprecated and no longer receives updates. All examples use compose.yaml naming. If you’re still on V1, upgrade with apt install docker-compose-plugin or check your distro’s docs.
Dockge is optional but recommended. It gives you a web UI to manage your compose stacks without SSH-ing into the server. Check Dockge Install - Docker Compose Manager for Self-Hosting for the full setup. If you prefer a home server over a VPS, have a look at the best Mini PCs for home server. An ASUS DC510 works well for this kind of stack.
Familiarize yourself with these essential Docker commands before moving on. They’ll help with troubleshooting.
2. Create the project directory and config files
Docker bind-mounts behave badly when the source file doesn’t exist. Docker creates a directory instead of a file, and your PHP config won’t load. Create the config files first:
# Navigate to where the stack will live
cd /opt/stacks/wordpress
# Create the config directory and empty files
mkdir -p config
touch ./config/wp_php.ini
touch ./config/pma_php.ini
touch ./config/pma_config.php
# Set ownership so containers can write to volumes
# WordPress runs as UID 1000 inside the container
chown -R 1000:1000 ./config
File permissions matter
If you skip creating these files first, Docker will create them as directories, and your PHP config won’t load. After the first docker compose up, also check ownership of the data directories: chown -R 1000:1000 ./wp-app ./db_data ./backups to avoid permission denied errors inside WordPress.
Verify the files were created correctly:
ls -la ./config/
You should see regular files (-rw-r--r--), not directories (drwxr-xr-x).
3. Docker Compose file: WordPress, MySQL 8.4 LTS and phpMyAdmin
Here’s the full compose.yaml with all five services. I’ll explain the key decisions after the code.
services:
wp:
image: wordpress:php8.3
restart: unless-stopped
ports:
- 5010:80
volumes:
- ./config/wp_php.ini:/usr/local/etc/php/conf.d/conf.ini
- ./wp-app:/var/www/html
environment:
WORDPRESS_DB_HOST: wp-db:3306
WORDPRESS_DB_NAME: "${DB_NAME}"
WORDPRESS_DB_USER: "${DB_USER}"
WORDPRESS_DB_PASSWORD: "${DB_PASSWORD}"
WORDPRESS_TABLE_PREFIX: "wp_"
WORDPRESS_CONFIG_EXTRA: |
define('FS_METHOD', 'direct');
depends_on:
wp-db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/wp-admin/install.php"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
wp-db:
image: mysql:8.4
volumes:
- ./db_data:/var/lib/mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
MYSQL_DATABASE: "${DB_NAME}"
MYSQL_USER: "${DB_USER}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 512M
pma:
image: phpmyadmin:latest
ports:
- 5011:80
volumes:
- ./config/pma_php.ini:/usr/local/etc/php/conf.d/conf.ini
- ./config/pma_config.php:/etc/phpmyadmin/config.user.inc.php
restart: unless-stopped
environment:
PMA_HOST: wp-db
PMA_PORT: 3306
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
UPLOAD_LIMIT: 100M
depends_on:
- wp-db
wp-db-backup:
image: tiredofit/db-backup:4.1
volumes:
- ./backups:/backup
restart: unless-stopped
environment:
DB_TYPE: mysql
DB_HOST: wp-db
DB_NAME: "${DB_NAME}"
DB_USER: "${DB_USER}"
DB_PASS: "${DB_PASSWORD}"
DB_BACKUP_INTERVAL: 720
DB_CLEANUP_TIME: 72000
CHECKSUM: SHA1
COMPRESSION: ZSTD
CONTAINER_ENABLE_MONITORING: "false"
depends_on:
- wp-dbservices:
wp:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
ports:
- 5010:80
volumes:
- ./config/wp_php.ini:/usr/local/etc/php/conf.d/conf.ini
- ./wp-app:/var/www/html
environment:
WORDPRESS_DB_HOST: wp-db:3306
WORDPRESS_DB_NAME: "${DB_NAME}"
WORDPRESS_DB_USER: "${DB_USER}"
WORDPRESS_DB_PASSWORD: "${DB_PASSWORD}"
WORDPRESS_TABLE_PREFIX: "wp_"
WORDPRESS_CONFIG_EXTRA: |
define('FS_METHOD', 'direct');
define('WP_REDIS_HOST', 'redis-wp');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_DATABASE', 0);
depends_on:
wp-db:
condition: service_healthy
redis-wp:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/wp-admin/install.php"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
wp-db:
image: mysql:8.4
volumes:
- ./db_data:/var/lib/mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
MYSQL_DATABASE: "${DB_NAME}"
MYSQL_USER: "${DB_USER}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 512M
pma:
image: phpmyadmin:latest
ports:
- 5011:80
volumes:
- ./config/pma_php.ini:/usr/local/etc/php/conf.d/conf.ini
- ./config/pma_config.php:/etc/phpmyadmin/config.user.inc.php
restart: unless-stopped
environment:
PMA_HOST: wp-db
PMA_PORT: 3306
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
UPLOAD_LIMIT: 100M
depends_on:
- wp-db
wp-db-backup:
image: tiredofit/db-backup:4.1
volumes:
- ./backups:/backup
restart: unless-stopped
environment:
DB_TYPE: mysql
DB_HOST: wp-db
DB_NAME: "${DB_NAME}"
DB_USER: "${DB_USER}"
DB_PASS: "${DB_PASSWORD}"
DB_BACKUP_INTERVAL: 720
DB_CLEANUP_TIME: 72000
CHECKSUM: SHA1
COMPRESSION: ZSTD
CONTAINER_ENABLE_MONITORING: "false"
depends_on:
- wp-db
redis-wp:
image: redis:7-alpine
restart: unless-stopped
volumes:
- ./redis_data:/data
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 128MDo NOT use mysql:latest
mysql:latest now tracks MySQL 9.x Innovation releases, short-lived versions with only ~3 months of support per minor release. For WordPress in production, use mysql:8.4 (LTS, supported until April 2032). The mysql:lts tag also works as a moving LTS pointer.
MySQL Innovation vs LTS
Since July 2024, MySQL uses a dual release track. Innovation releases (9.x) ship new features quarterly but have a short support window. Fine for testing, bad for production. LTS releases (8.4, future 9.7) get 5 years of premier support plus 3 years of extended support. Always use LTS for anything that stores data you care about.
Key decisions in this compose file:
wordpress:php8.3instead ofwordpress:latest: pins PHP to 8.3, which is the WordPress-recommended version. Thelatesttag also ships PHP 8.3 as of mid-2025, but pinning the tag avoids surprises when the default changes.mysql:8.4: the current LTS release, supported until 2032.redis:7-alpine: pinned version, Alpine-based for a smaller image (~30MB vs ~130MB).tiredofit/db-backup:4.1: pinned major version. This image is migrating tonfrastack/container-db-backup. The old one still works fine but watch for the new release.COMPRESSION: ZSTD: the new default in db-backup, faster compression and decompression than GZ.FS_METHOD: direct: tells WordPress to write files directly instead of using FTP, which doesn’t work in Docker.- Health checks: MySQL has a
mysqladmin pingcheck, WordPress has acurlcheck. Thedepends_on: condition: service_healthymeans WordPress waits for MySQL to be actually ready, not just started. - Resource limits: keeps each container from eating all your RAM. Adjust based on your VPS size.
For production, consider Docker Compose secrets instead of .env files. They’re more secure and don’t leave credentials in shell history or process listings.
4. Configure the .env file and security keys
Create a .env file in the same directory as your compose.yaml:
DB_NAME='wordpress'
DB_USER='wp'
DB_PASSWORD='use-a-strong-random-password-here'
DB_ROOT_PASSWORD=another-strong-random-password
Now add the WordPress security salts. These are 8 cryptographic keys that WordPress uses to encrypt cookies and authentication tokens. The official Docker image generates unique random SHA1 hashes from whatever values you provide. It’s a free security upgrade.
Free security upgrade
Adding WordPress security salts costs nothing and makes session hijacking significantly harder. The Docker image reads these environment variables and writes the corresponding define() constants into wp-config.php on first boot.
Add these to your .env file:
WORDPRESS_AUTH_KEY='put-unique-phrase-here'
WORDPRESS_SECURE_AUTH_KEY='put-unique-phrase-here'
WORDPRESS_LOGGED_IN_KEY='put-unique-phrase-here'
WORDPRESS_NONCE_KEY='put-unique-phrase-here'
WORDPRESS_AUTH_SALT='put-unique-phrase-here'
WORDPRESS_SECURE_AUTH_SALT='put-unique-phrase-here'
WORDPRESS_LOGGED_IN_SALT='put-unique-phrase-here'
WORDPRESS_NONCE_SALT='put-unique-phrase-here'
Generate real random values from the official WordPress salt generator:
Generate WP Salts →Replace each put-unique-phrase-here with the generated values. Don’t reuse these across installations.
If you’re using Dockge, you can add these as environment variables in the stack config instead of a .env file. For production setups, look at Docker Compose secrets or the _FILE environment variable variants the WordPress image supports (e.g., WORDPRESS_DB_PASSWORD_FILE=/run/secrets/wp-db-password).
5. Start WordPress in Docker
If you’re using Dockge, save the compose file and click Start. Otherwise:
docker compose up -d
Watch the logs to catch any startup errors:
docker compose logs -f
Wait about 30 seconds, then check that all containers are healthy:
docker compose ps
You should see all services with Up status. If you included health checks, the wp-db service should show (healthy) after a few seconds, and the wp service after about 30 seconds.
Startup order matters
The depends_on: condition: service_healthy on the wp service means WordPress won’t start until MySQL passes its health check. This prevents the common “Error establishing a database connection” race condition you get with plain depends_on.
If something fails:
- Port 5010 or 5011 already in use:
lsof -i :5010to find what’s occupying it, then change the port mapping - MySQL health check keeps failing:
docker compose logs wp-db(look for authentication or config errors) - WordPress can’t connect to DB: verify the
.envvalues match between thewpandwp-dbservices
6. Configure Cloudflare Tunnels for SSL
Cloudflare Tunnels give you SSL and DDoS protection without opening ports on your firewall or managing certificates.
Updated dashboard path
The Cloudflare dashboard path has changed. Navigate to Zero Trust → Networks → Tunnels, not the old “Access → Tunnels” path that older guides reference.
In the Cloudflare Zero Trust dashboard:
- Go to Zero Trust → Networks → Tunnels
- Select your tunnel (or create one with
cloudflared) - Add a hostname mapping your domain to
http://localhost:5010 - Save — Cloudflare handles SSL automatically

You can add a second hostname for phpMyAdmin on a subdomain (e.g., pma.yourdomain.com) pointing to http://localhost:5011. I’d recommend this over exposing port 5011 directly.
Verify the tunnel works:
curl -I https://yourdomain.com
You should get a 200 or 301 response with cf-ray and server: cloudflare headers.
502 Bad Gateway? WordPress container isn’t running or the port mapping is wrong. Run docker compose ps and check that the wp service is Up.
You can also use Traefik v3 as a reverse proxy if you prefer managing SSL yourself, or CloudPanel with Dockge for a different reverse proxy approach.
7. Complete the WordPress installation
Open your domain in the browser (or http://your-server-ip:5010 if you haven’t set up Cloudflare yet). You’ll see the WordPress installation wizard:

Choose your language, create an admin account, and you’re in. After that, configure permalinks under Settings → Permalinks (I use “Post name” for most sites) and start adding themes and plugins.
8. Customize PHP settings for WordPress in Docker
Edit ./config/wp_php.ini to tune PHP for WordPress:
file_uploads = On
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300
max_input_time = 1000
Bump memory_limit to 512M and upload_max_filesize to 128M if you’re running WooCommerce or uploading large media files.
After editing, restart the WordPress container:
docker compose restart wp
PHP version in the WordPress image
The wordpress:latest image ships PHP 8.2 as of mid-2025. If you followed this guide, you’re using wordpress:php8.3 which is the WordPress-recommended minimum. The wordpress:php8.4 tag is also available and fully supported by WordPress 6.7+. Check your version with: docker exec wp php -v
Database management and backups
9. Access phpMyAdmin for database management
Access phpMyAdmin at http://your-server-ip:5011 (or via a Cloudflare tunnel subdomain). Log in with the database credentials from your .env file — the DB_USER and DB_PASSWORD values.
The UPLOAD_LIMIT: 100M in the compose file lets you import larger database dumps through the phpMyAdmin UI.
Don't expose phpMyAdmin publicly
phpMyAdmin gives full access to your database. In production, firewall off port 5011 and only access it through a Cloudflare Tunnel with an Access policy, or use SSH tunneling: ssh -L 5011:localhost:5011 your-server-ip.
10. Verify automatic database backups
The wp-db-backup container runs on a schedule defined by DB_BACKUP_INTERVAL: 720 (every 12 hours) and cleans up backups older than DB_CLEANUP_TIME: 72000 minutes (~50 days).
Check the backup directory:
ls -ltr ./backups/
You should see files like:
-rw------- 1 10000 10000 495 Jul 17 09:26 mysql_wordpress_wp-db_20250717-092619.sql.zst
-rw------- 1 10000 10000 87 Jul 17 09:26 mysql_wordpress_wp-db_20250717-092619.sql.zst.sha1
lrwxrwxrwx 1 10000 10000 44 Jul 17 09:26 latest-mysql_wordpress_wp-db -> mysql_wordpress_wp-db_20250717-092619.sql.zst
Backup image migration
The tiredofit/db-backup image is migrating to nfrastack/container-db-backup. The current image (pinned at 4.1) still works fine. Watch for the new release if you’re setting this up after mid-2026. The compression extension changed from .sql.gz to .sql.zst (ZSTD is faster than GZ).
For full site backups (files + database), pair this with a WordPress backup plugin — see Best Free WordPress Backup Plugins for options that handle themes, plugins, and uploads too.
11. How to restore a database backup
Test your restores
A backup you can’t restore is not a backup. Run through this procedure at least once after initial setup to make sure it works.
To restore from a compressed backup:
# For ZSTD-compressed backups (new default)
zstd -d ./backups/latest-mysql_wordpress_wp-db -c | docker exec -i wp-db mysql -u "${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}"
# For GZ-compressed backups (if you haven't updated compression)
zcat ./backups/latest-mysql_wordpress_wp-db | docker exec -i wp-db mysql -u "${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}"
To verify backup integrity before restoring, check the SHA1 sidecar file:
cd ./backups
sha1sum -c mysql_wordpress_wp-db_20250717-092619.sql.zst.sha1
After restoring, open WordPress admin and confirm your posts and pages are present.
Common errors:
- “Access denied” — wrong credentials or missing quotes around the password
- “Unknown database” — the
DB_NAMEin the restore command doesn’t match the backup - “ERROR 2006 (HY000)” — MySQL server has gone away, the dump is too large; increase
max_allowed_packetin MySQL config
Optional performance improvements
12. Add Redis Object Cache to WordPress in Docker
Redis requires a PHP extension
The official WordPress Docker image does NOT include the Redis PHP extension. Adding the redis-wp service to your compose file is not enough — you must also install the phpredis extension or use the Predis pure-PHP library. Without this, the Redis Object Cache plugin will show “Not connected.”
There are three ways to add Redis support. I recommend the custom Dockerfile approach — it’s the cleanest.
Create a Dockerfile in the same directory as your compose.yaml:
FROM wordpress:php8.3
RUN pecl install redis && docker-php-ext-enable redisThen change the wp service in your compose file from image: wordpress:php8.3 to:
wp:
build:
context: .
dockerfile: DockerfileThe WORDPRESS_CONFIG_EXTRA in the full stack compose file (Tab 2 in Step 3) already includes the Redis host and port defines. Rebuild with:
docker compose up -d --buildUse fazalfarhan01/wordpress-redis — a community image that bundles the Redis extension:
wp:
image: fazalfarhan01/wordpress-redis:php8.3No Dockerfile needed, but you’re trusting a third-party image. Check the Docker Hub page for the latest tags.
The Redis Object Cache plugin supports the Predis pure-PHP library as an alternative to the phpredis extension. Install it via Composer inside the container:
docker exec wp bash -c "curl -sS https://getcomposer.org/installer | php && php composer.phar require predis/predis"Then in the plugin settings, switch the client to “Predis.” This is slower than the native extension but requires no image customization.
After activating the Redis Object Cache plugin (by Till Krüss), go to Settings → Redis in WordPress admin and click Enable Object Cache. The status should show “Connected.”
Not connecting? Check that:
- The Redis container is running:
docker compose ps redis-wp - The host/port in
WP_REDIS_HOST/WP_REDIS_PORTmatch the service name and port - The PHP Redis extension is actually installed:
docker exec wp php -m | grep redis
For maximum performance, combine Redis caching with Varnish and Cloudflare — see How to Speed Up WordPress with Cloudflare, Varnish and Redis.
Production hardening
13. Docker health checks for production
Health checks are already configured in the compose file from Step 3. Here’s what they do:
- MySQL (
wp-db): Runsmysqladmin pingevery 10 seconds. After 5 failed checks, the container is marked unhealthy. This prevents WordPress from connecting before MySQL is ready. - WordPress (
wp): Curls the install page every 30 seconds. Confirms the web server and PHP are responding.
Monitor health status:
docker compose ps
All services should show (healthy) in the STATUS column. If a service is (unhealthy), check its logs: docker compose logs <service-name>.
The restart: unless-stopped policy means containers auto-restart on failure or server reboot, but stay stopped if you manually stop them.
14. WP-CLI container for WordPress maintenance
The official wordpress:cli image gives you command-line access to WordPress without installing anything extra. Run commands against your existing WordPress container:
docker run -it --rm \
--volumes-from wp \
--network container:wp \
wordpress:cli \
wp plugin list
Common WP-CLI commands
Plugin management:
# List installed plugins
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp plugin list
# Update all plugins
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp plugin update --all
# Deactivate a plugin
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp plugin deactivate plugin-nameUser management:
# List users
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp user list
# Reset a user password
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp user update admin --user_pass=newpasswordDatabase operations:
# Export database
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp db export /var/www/html/backup.sql
# Search and replace URLs (useful after domain changes)
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp search-replace 'http://old-domain.com' 'https://new-domain.com' --skip-columns=guidCore updates:
# Check current version
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp core version
# Update WordPress core
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp core updateThe FS_METHOD: direct define in WORDPRESS_CONFIG_EXTRA is required for WP-CLI (and WordPress itself) to write files in Docker without FTP.
“Error: This does not seem to be a WordPress install”? You’re missing --volumes-from wp or the container name is wrong. Check with docker compose ps.
15. Security hardening checklist
- MySQL 8.4 LTS pinned (not
mysql:latest) - WordPress security salts set in
.env FS_METHOD: directinWORDPRESS_CONFIG_EXTRA- Port 5011 (phpMyAdmin) firewalled off — access only via Cloudflare Tunnel or SSH
- Cloudflare Access policy in front of phpMyAdmin subdomain
- Database passwords are strong random strings, not dictionary words
- WordPress table prefix changed from
wp_if you’re paranoid (set inWORDPRESS_TABLE_PREFIX) - XML-RPC disabled if you’re not using Jetpack (add to
.htaccessor use a plugin) - Resource limits set in compose file to prevent runaway containers
- Regular backup restores tested (not just backup creation)
Already covered
Most of these are already handled by following Steps 3-6 of this guide. This checklist is here for reference and for when you’re auditing your setup later.
Monitor your server to detect anomalies early — see How To Monitor Server and Docker Resources for setting up resource monitoring with tools like Beszel or Netdata.
16. MariaDB as a MySQL alternative
MariaDB is fully compatible with WordPress, has a lighter memory footprint, and is recommended alongside MySQL in the WordPress Hosting Handbook. Many self-hosters prefer it.
To swap, change one line in your compose file:
# Replace this:
wp-db:
image: mysql:8.4
# With this:
wp-db:
image: mariadb:11.4
Same env vars, same everything
MariaDB uses the same environment variables as MySQL (MYSQL_ROOT_PASSWORD, MYSQL_DATABASE, etc.). No other changes needed — just swap the image tag. MariaDB 11.4 LTS is supported until May 2029.
17. Updating WordPress in Docker
Two strategies depending on how much control you want:
Strategy 1: Self-managing (default)
WordPress auto-updates itself inside the volume. This is the default behavior — WordPress checks for updates and applies them without you touching Docker. Simple, but your infrastructure isn’t immutable.
Strategy 2: Pinned version (recommended for production)
Pin the WordPress image version in your compose file, disable auto-updates, and control when you update:
wp:
image: wordpress:php8.3:6.8
Add to WORDPRESS_CONFIG_EXTRA:
define('WP_AUTO_UPDATE_CORE', false);
When you’re ready to update, change the version tag and redeploy:
docker compose pull
docker compose up -d
Your wp-app volume persists all WordPress files, themes, plugins, and uploads. The container is just the runtime — your data lives on the host.
“Another update is in progress”? This is a stuck transient. Clear it with WP-CLI:
docker run -it --rm --volumes-from wp --network container:wp wordpress:cli wp option delete core_updater.lock
18. What’s Next
Your WordPress Docker stack is running. A few things to do from here:
- Close firewall ports — if you’re using Cloudflare Tunnels, block ports 5010 and 5011 at the firewall level so only the tunnel can reach them. Access is only through your domain.
- Set up monitoring — monitor your server and Docker resources to catch CPU spikes, disk fill-ups, and container restarts before they become problems.
- Explore self-hosted panels — if you want a broader management interface, check the best self-hosted server panels for options beyond Dockge.
- Master Docker commands — bookmark these essential Docker commands for troubleshooting containers, cleaning up disk space, and managing images.
- Install themes and plugins — WordPress is ready for your content. Start with a lightweight theme and add only the plugins you need.
Conclusion
You now have a production-ready WordPress stack running in Docker with MySQL 8.4 LTS (supported until 2032), phpMyAdmin for database management, automated backups with ZSTD compression, optional Redis object caching, and SSL through Cloudflare Tunnels — all from a single compose.yaml file.
The most important next step is testing your backups. A backup you’ve never restored is a gamble, not a strategy. Run through the restore procedure in Step 11 at least once, then set a calendar reminder to test it quarterly.
If something breaks, docker compose logs is your best friend. Most issues come down to port conflicts, permission errors, or MySQL not being ready when WordPress tries to connect — all of which the health checks in this setup are designed to catch.


