WordPress and MySQL in Docker is the classic two-service Compose stack. Done right, it survives container removal, mounts wp-content cleanly so you can edit themes and plugins on the host, and connects via Compose service-name DNS. Done wrong, the first docker compose down wipes everything and the second time you mount wp-content it overwrites the default theme with an empty directory.
How do I run WordPress in Docker?
services:
wordpress:
image: wordpress:php8.4-apache
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wp
WORDPRESS_DB_PASSWORD: change-me
WORDPRESS_DB_NAME: wordpress
volumes:
- wp-content:/var/www/html/wp-content
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: root-pass
MYSQL_DATABASE: wordpress
MYSQL_USER: wp
MYSQL_PASSWORD: change-me
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-proot-pass"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
volumes:
wp-content:
db-data:Save as docker-compose.yml, run docker compose up -d, visit http://localhost:8080, walk through the install wizard. Both services persist via named volumes; both restart automatically; Compose waits for MySQL to be healthy before starting WordPress.
Configure ports and credentials. Compose values update inline above.
The two volumes that matter
db-data on /var/lib/mysql — the MySQL data directory. Without this, the next docker compose down removes the container and your entire WordPress database goes with it. With it, you can stop and start the stack as many times as you want; the database persists.
wp-content on /var/www/html/wp-content — WordPress's themes, plugins, and uploads. This is the directory you actually care about preserving — the rest of WordPress core can be re-installed; wp-content is your customization. The image's default wp-content is copied into the named volume on first start, so default themes and plugins are still there.
A third option people sometimes do: mount the whole /var/www/html (the full WordPress install). That's wasteful — WordPress core changes only on version upgrades, and mounting all of it makes upgrades clunky. Mount wp-content only.
Bind mount wp-content for active development
When you're developing a theme or plugin, mount your local source over wp-content/themes/mytheme or wp-content/plugins/myplugin:
services:
wordpress:
# ...
volumes:
- wp-content:/var/www/html/wp-content
- ./mytheme:/var/www/html/wp-content/themes/mythemeEdit files on the host, refresh the browser, see changes. The named wp-content volume still holds the rest (other themes, uploads, plugins) and persists across container removal.
Practical usage: opening the admin and uploading a file
After docker compose up -d:
# Tail logs to see when WordPress is ready
docker compose logs -f wordpress
# Once it's running, the install wizard is at http://localhost:8080After the install:
- Admin:
http://localhost::host_port/wp-admin - Uploads end up in the
wp-contentvolume; they persist across container restarts and host reboots. - Database access from the host: the
dbservice isn't published by default (noports:block on it). Addports: ["3307:3306"]to thedbservice if you want to connect with a host-side MySQL client. Or use Adminer in a third container.
Run wp-cli inside the container
The official WordPress image doesn't ship wp-cli, but it's trivial to add:
docker compose exec wordpress \
bash -c 'curl -sO 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'
# Then any wp command:
docker compose exec -u www-data wordpress wp plugin list
docker compose exec -u www-data wordpress wp user list-u www-data runs wp-cli as the web-server user so files it creates have the right ownership.
For a one-shot wp-cli run, the dedicated image wordpress:cli is cleaner:
services:
cli:
image: wordpress:cli
user: "33:33" # www-data UID
volumes:
- wp-content:/var/www/html/wp-content
depends_on:
- db
profiles: ["cli"] # only starts when explicitly requesteddocker compose run --rm cli wp plugin listBackups
WordPress in Docker has two things to back up: the database and wp-content.
# Database dump
docker compose exec db \
mysqldump -u root -proot-pass --single-transaction wordpress \
> backup-db-$(date +%Y%m%d).sql
# wp-content as a tarball
docker run --rm \
-v wordpress_wp-content:/source:ro \
-v "$(pwd):/backup" \
alpine tar czf /backup/wp-content-$(date +%Y%m%d).tar.gz -C /source .(The named volume's full name is <projectname>_wp-content where projectname is the Compose project name, usually the directory name. docker volume ls shows the actual name.)
Common pitfalls
- Mounting
wp-contentand finding the default theme missing. The image's defaultwp-contentis copied into the volume only on first start. If the volume already exists and is empty, no copy happens. Either delete the empty volume and let it re-init, or copy the default theme in manually withdocker cp. - WORDPRESS_DB_HOST set to
localhost. Inside the WordPress container,localhostis the WordPress container's loopback, not MySQL's. Use the service name:db:3306. - No healthcheck on the DB, no
condition: service_healthy. WordPress starts before MySQL is ready and the install wizard errors out. Add the healthcheck and thedepends_on: conditionshown in the example. - Uploads owned by root. Without a writable wp-content volume, or with the wrong user mapping, the wizard can't write to
/var/www/html/wp-content/uploads. The default image runs Apache aswww-data(UID 33); permissions on the named volume are set correctly automatically. Bind mounts to host paths are the common source of UID mismatch. - Port 80 already in use. Pick a different host port (
8080:80). - Memory limit too low. The default
WORDPRESS_CONFIG_EXTRAdoesn't setWP_MEMORY_LIMIT; complex sites may need bumping. AddWORDPRESS_CONFIG_EXTRA: "define('WP_MEMORY_LIMIT', '256M');"to the env.
What to do next
- Run MySQL in Docker — the MySQL side in detail.
- Run Adminer in Docker — a lightweight DB browser to inspect this WordPress's database.
- Docker Compose: Getting Started — the full Compose article.
- Change a WordPress Password — the WP-CLI / SQL options apply identically inside the container.
FAQ
Sources
Authoritative references this article was fact-checked against.
- Official WordPress image — Docker Hubhub.docker.com
- WordPress server environment recommendationsmake.wordpress.org

