WPDC: a four-service WordPress stack in one compose file
Easy Wordpress development with Docker and Docker Compose
At a glance
- What is it?
- This repository is a Docker Compose file, not an application: WordPress, WP-CLI, phpMyAdmin and MySQL wired to two host folders, tuned for local theme and plugin work. It runs everything on latest tags and connects WordPress as the MySQL root user, which is fine on a laptop and wrong anywhere else.
- Who is it for?
- Adopt WPDC if you build WordPress themes or plugins locally and want a disposable stack that mounts your trunk directory straight into wp-content, since that volume is the whole reason to use this over a plain web server. Do not adopt it for staging, client demos on a shared host, or anywhere a real network exists, because the compose file publishes MySQL on port 3306, connects WordPress as root, and binds phpMyAdmin with the same root password.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 64 days ago.
- What is it written in?
- Mainly Shell, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 6, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What this repository actually is: four services and two folders
WPDC is a compose file with a README around it. There is no application code, no PHP, no build step, and the top level holds just docker-compose.yml, env.example, export.sh, a config directory and a .github folder. What you get is four services wired to each other: wp running wordpress, wpcli running wordpress:cli, pma running phpMyAdmin, and db running MySQL.
The design goal is stated in one line: easy WordPress development with Docker and Docker Compose. Everything else follows from that being a local developer tool rather than a deployment artefact.
The shape of the compose file is worth reading closely, because it tells you what the project assumes. Images are all floating tags (wordpress:latest, phpmyadmin:latest, mysql:latest). The wp service mounts ./wp-app at /var/www/html, which is the location of your WordPress project, plus a php ini file from ./config. The wpcli service mounts the same two paths, which is why the CLI sees the same code as the web server. pma mounts its own ini and a config.user.inc.php, and raises UPLOAD_LIMIT to 50M. Only the db service carries a command argument, forcing --character-set-server=utf8mb4.
Database access is the root account, on a published port
The environment block is where the security posture of this stack is decided, and it is not subtle. The wp and wpcli services both set WORDPRESS_DB_USER: root and WORDPRESS_DB_PASSWORD from DB_ROOT_PASSWORD. That is the MySQL superuser account, not a scoped WordPress user.
The db service publishes its port to the host as ${IP}:3306:3306, and the wp service publishes ${IP}:${PORT}:80. phpMyAdmin is published as ${IP}:8080:80 and defaults to the same root credentials, as the README confirms by telling you the password is whatever you put in .env.
The mitigating detail is the IP variable. By default it resolves to 127.0.0.1, so nothing is reachable off the machine. But the README explicitly invites you to change that IP to run several containers side by side, and the same edit that makes room for a second stack also decides whether a MySQL superuser password is exposed to your network. If you change IP, change the port mappings too, or do not publish db at all when you only need phpMyAdmin to reach it over the internal network.
For a local machine this is acceptable and convenient. For anything else it is the first thing to fix.
Plugin and theme work happens through bind mounts, not rebuilds
The most useful part of the compose file for real work is the pair of commented volume lines under the wp service. Uncommenting one mounts a local trunk directory into the container, so PHP edits on the host appear in the container immediately with no image rebuild:
volumes:
- ./theme-name/trunk/:/var/www/html/wp-content/themes/theme-nameThe plugin variant points at wp-content/plugins/plugin-name instead. Both default to the trunk layout used by the WordPress.org hosting model, which tells you the intended user: someone maintaining a public plugin or theme and testing against a real WordPress rather than a hand-built sandbox.
Because the whole of ./wp-app is already mounted at /var/www/html, those lines are only needed when your code lives outside the wp-app folder, for example when the plugin repository sits beside this compose file. Editing wp-config.php to point WP_HOME and WP_SITEURL at a local host entry is the companion step:
define('WP_HOME','http://wp-app.local');
define('WP_SITEURL','http://wp-app.local');The README recommends adding that name to your hosts file, which is the standard trick for keeping cookie domains and asset URLs consistent with a project.
Getting it running from env.example to localhost
The setup path is four steps. Clone or copy the files, edit docker-compose.yml if you need a different IP address or want to switch MySQL for MariaDB, copy the environment file, then start.
cp env.example .env
docker compose upThe .env file holds the default IP address, the MySQL root password and the WordPress database name, and those same variables are interpolated into the compose file rather than hardcoded. That is a genuinely good pattern for a template: the credentials live in one file you can delete.
Running up creates wp-data for database dumps and wp-app for the WordPress application, next to the compose file. The site is then reachable at http://127.0.0.1 by default, and phpMyAdmin at http://127.0.0.1:8080.
Day-to-day commands are the standard trio: docker compose start to bring an existing set back up, docker compose stop to halt them, and docker compose down to remove the containers. Add -v to down when you want the database volume gone as well, which is the documented way to reset state.
WP-CLI as a compose service, and the alias that makes it usable
The wpcli service exists so that WordPress command line work happens inside the same container image family as the site, with the same mounts and the same database environment. A typical one-shot install run:
docker compose run --rm wpcli core install --url=http://localhost --title=test --admin_user=admin [email protected]The --rm flag is the important part: it throws the throwaway container away afterwards, so your next command starts clean. Listing plugins reads the same way, with docker compose run --rm wpcli plugin list.
That verbosity is why the README recommends an alias:
alias wp="docker compose run --rm wpcli"With it, wp plugin list is the whole command. Since the wpcli service declares depends_on both db and wp, a command run this way is at least ordered after those containers exist, though not after they are healthy.
Database dumps go through a shell script in the repository, run as ./export.sh. Its output lands in wp-data, which is why that folder exists at all rather than being mounted into a database service.
Structural gaps: floating tags, no healthchecks, no restart policy
Three omissions matter for anyone planning to run this longer than a single afternoon.
The first is version pinning. wordpress:latest, phpmyadmin:latest and mysql:latest all float. A rebuild months later can pull a different database engine than the one your wp-data dump came from, and since the db service forces --character-set-server=utf8mb4 but nothing else about its configuration, the rest is whatever the image default is that week. Pin the three tags before the first run if you care about reproducibility.
The second is startup ordering. depends_on lists service names without any condition, and the shown configuration has no healthcheck on db and no restart policy on wp. WordPress that starts before MySQL is accepting connections will typically fail its bootstrap, and Compose will not retry it for you. Adding a mysqladmin healthcheck and a depends_on condition would make the stack self-healing; the repository as shipped does not.
The third is arm64. The compose file carries a commented platform: linux/x86_64 line with a note about Apple Silicon. On an arm Mac this is the difference between a working up and an exec format error, and whether you need it depends on which images currently publish arm64 builds.
Alternatives: wp-env, a hand-rolled compose file, or a managed host
The closest comparison is wp-env, the WordPress project's own Docker environment, which also ships as an npm package and defaults to a bind-mounted source tree so plugin code edits are picked up directly. The difference is who drives it: wp-env wraps the compose file for you and speaks plugin and theme checkouts, whereas this repository hands you four services and expects you to read the YAML. For a plugin author who wants a running site in one command, wp-env removes the configuration step; for someone who wants phpMyAdmin, a WP-CLI service and control over the database image, this file has all three in about fifty lines.
Writing your own compose file is the other option, and it is not much work, which is the honest assessment of what this project provides. What the repository adds, last pushed on 2026-08-09, is documentation of the parts people usually get wrong: the wp_php.ini and pma_config.php mounts, the utf8mb4 server character set, the WP_HOME and WP_SITEURL overrides, and the export.sh dump workflow.
Managed WordPress hosting sits at the other end. It removes all of this, including the root password problem, at the cost of a persistent environment you cannot reset with docker compose down -v, and of no direct bind mount for local plugin iteration.
Editorial conclusion
Adopt WPDC if you build WordPress themes or plugins locally and want a disposable stack that mounts your trunk directory straight into wp-content, since that volume is the whole reason to use this over a plain web server. Do not adopt it for staging, client demos on a shared host, or anywhere a real network exists, because the compose file publishes MySQL on port 3306, connects WordPress as root, and binds phpMyAdmin with the same root password. Verify three things in order. First, replace every latest tag with a pinned image version, starting with mysql:latest, since a pull that changes the database engine under an existing wp-data dump is the most expensive mistake available here. Second, decide whether the commented platform: linux/x86_64 line is needed on your machine and uncomment it only if the images have no arm64 build. Third, read env.example before copying it to .env, since DB_ROOT_PASSWORD is consumed by both WordPress and phpMyAdmin. Start with cp env.example .env and docker compose up, then add the alias wp="docker compose run --rm wpcli" so plugin list works without retyping the service name.
Frequently asked questions
How do I set up WordPress with Docker Compose using WPDC?
Copy the files, run cp env.example .env to create the environment file, then run docker compose up in that folder. The compose file creates wp-app for the WordPress code and wp-data for database dumps, and the site is served at http://127.0.0.1 by default.
How do I run WP-CLI in this WordPress Docker setup?
Through the wpcli service, for example docker compose run --rm wpcli plugin list. The README recommends an alias wp="docker compose run --rm wpcli" so you can write wp plugin list instead.
How do I develop a WordPress plugin with this compose file?
Uncomment the plugin volume line under the wp service so ./plugin-name/trunk/ is mounted at /var/www/html/wp-content/plugins/plugin-name. Edits on the host then appear in the container without rebuilding the image.
How do I access the database in this WordPress stack?
phpMyAdmin is published at http://127.0.0.1:8080 with the username root and the password from your .env file. MySQL itself is published on ${IP}:3306:3306, which resolves to 127.0.0.1 unless you change IP.
Does this compose file work on Apple Silicon?
The compose file ships a commented platform: linux/x86_64 line under the db service with a note about Apple Silicon. Whether you need it depends on whether the mysql image currently publishes an arm64 build.