Developing WordPress sites with Docker
I recently set up a new WordPress based website and local Docker-based development environment. This post documents what I did, so that I can do it again next time! As I’m not in the WordPress world, many things are strange to me and I’m indebted to Jenny Wong for pointing me in the right direction on numerous occasions and being very patient with my questions! Thanks Jenny!
Project organisation
There’s always ancillary files and directories in a project that aren’t part of the actual website, so I have put the WordPress site in a subdirectory called app and then I have room for other stuff. My project’s root directory looks like this:
$ tree . --dirsfirst -FL 1
.
├── app/
├── bin/
├── data/
├── docker/
├── README.md
└── docker-compose.yml
This is what each item is for:
- app/ – The WordPress application files are in this directory.
- bin/ – Useful command-line scripts
- data/ – MySQL dump files go here.
- docker/ – Files required by the Docker setup are in this directory.
- README.md – Every project needs a README!
- docker-compose.yml – Development orchestration config file.
I put everything into git, with a .gitignore file to ignore everything in data along with various other WordPress files/directories that shouldn’t in version control.
Docker
A pair of Docker containers is used to run the site locally for development. I’m slowly getting my feet wet with Docker, so I’m not sure if this is the best way to do things. The docker-compose command allows you to spin up multiple containers in one go and join them together. This is done via the docker-compose.yml file.
Mine looks like this:
docker-compose.yml:
version: '3'
services:
db:
image: mysql:5.7
ports:
- 127.0.0.1:3306:3306
command: [
'--default_authentication_plugin=mysql_native_password',
'--character-set-server=utf8mb4',
'--collation-server=utf8mb4_unicode_ci'
]
volumes:
- db_data:/var/lib/mysql
environment:
MYSQL_DATABASE: wordpress
MYSQL_ROOT_PASSWORD: 123456
wp:
build:
context: ./docker
dockerfile: Dockerfile-wp
ports:
- 127.0.0.1:80:80
volumes:
- ./docker/php.conf.ini:/usr/local/etc/php/conf.d/conf.ini
- ./app:/var/www/html
depends_on:
- db
links:
- db
environment:
DB_NAME: wordpress
DB_USER: root
DB_PASSWORD: 123456
DB_HOST: db
WP_DEBUG: 1
LIVE_URL: https://project1.com
DEV_URL: http://dev.project1.com
volumes:
db_data: # store database into a volume so that we can pause the containers
There are two containers: wp for the Apache/PHP and db for the MySQL.
The db container
The db container uses the default Docker MySQL container. I picked version 5.7 as that’s what’s running in my live environment. As this is a single-purpose development container, I just use the MySQL root user and set its password.
I want to persist the MySQL database between invocations of the container, so to do this, I create a volume called db_data and then map the /var/lib/mysql directory to that volume. I also expose MySQL on 3306 so that I can connect to it from my desktop.
The wp container
For the wp container, I start with the default Docker WordPress container and add XDebug and the WP-CLI to it. This is done in the ./docker/Dockerfile-wp file:
./docker/Dockerfile-wp:
FROM wordpress:php7.3-apache
# Install xdebug
RUN pecl install xdebug && docker-php-ext-enable xdebug
# Install Less for WP-CLI
RUN apt-get update && apt-get -y install less
# Install WP-CLI
RUN curl -s -o /usr/local/bin/wp \
https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
&& chmod +x /usr/local/bin/wp
docker-compose will now create a container from our Dockerfile-wp and use that.
I map the docker/php.conf.ini into the container so that the PHP picks up our additional php.ini configuration settings:
docker/php.conf.ini:
upload_max_filesize = 10M
post_max_size = 10M
xdebug.overload_var_dump = 1
xdebug.remote_enable = 1
xdebug.remote_autostart = 0
xdebug.remote_connect_back = 0
xdebug.remote_host = host.docker.internal
Note that I’m on Mac, so the magic host.docker.internal domain name is available so that XDebug works. Incredibly, this hostname isn’t available on Linux (yet?), so you’ll have to find a workaround if you’re on Linux.
For the wp container, I also set some environment variables for WordPress configuration. I also set LIVE_URL and DEV_URL for use later when restoring a database dump file from live.
Hostname
Set up a host name pointing at 127.0.0.1 in your /etc/hosts for the dev site. I use dev.live-site.name personally.
127.0.0.1 dev.project1.com
Start up
Start up with:
$ docker-compose up
You can now go to http://dev.project1.com and start work. Put WordPress in to app/ and the install process will start and create a database for you.
ctrl+c will stop them.
To rebuild the containers: docker-compose up --force-recreate --build
To delete the db_data volume: docker-compose down -v
The database
There’s a couple of obvious database operations that we need to perform: exporting the data to dump file and restoring a dump file. I’ve implemented a couple of scripts for these tasks in my bin directory.
Exporting the database
Firstly, to export the database into a MySQL dump file from the db container:
bin/export-db:
#!/usr/bin/env bash
this_dir=$(cd `dirname $0` && pwd)
file="$this_dir/../data/dump.sql"
# Create dump file
cmd='exec mysqldump "$MYSQL_DATABASE" -uroot -p"$MYSQL_ROOT_PASSWORD"'
docker-compose exec db sh -c "$cmd" > $file
# Remove password warning from the file
sed -i '.bak' 1,1d $file && rm "$file.bak"
The container already has the database and password as environment variables so we can avoid hard-coding them. As mysqldump gets upset about using the password on the command line nowadays, we remove its warning with sed.
Restoring a database dump
The master database for my project is the live one and I want to use it on dev, so I have script that retrieves a dump from live and puts it into the data directory. I then restore it using:
$ bin/restore-db live-dump.sql
This is the restore script:
bin/restore-db:
#!/usr/bin/env bash
file=$1
if [ -z "$file" ]; then
echo "USAGE: restore-db <filename>"
exit 1;
fi
# Restore database to db container
cmd='exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" "$MYSQL_DATABASE"'
docker exec -i $(docker-compose ps -q db) sh -c "$cmd" < $file
# Replace LIVE_URL using WP-CLI in wp container
cmd='wp --allow-root search-replace "$LIVE_URL" "$DEV_URL" --skip-columns=guid'
docker-compose exec wp sh -c "$cmd"
We have two operations in this script.
Firstly we need to restore the database. This is complicated slightly by docker-compose exec not providing an input TTY, so we have to use docker exec and find the container from the compose name (db) with docker-compose ps -q db. Again we take advantage of the environment variables so that we don’t need to store the database name or password in the script.
Secondly, we need to replace all instances of the live domain name in the data (i.e. fully qualified links in posts and also config data) with the dev domain name. This is done with the WP-CLI command:
$ wp --allow-root search-replace $from $to --skip-columns=guid
We need --allow-root as our container runs with user root. $from and $to are our search and replace terms and finally we need to ensure we don’t change any GUIDs using --skip-columns=guid. (If ever there should have been a default column to skip, guid was it!)
The end result is that we can restore our database and start working on the next feature.
wp-config.php
WordPress uses a set of constants in the wp-config.php file for configuration. I updated my wp-config.php to look for environment variables in preference:
/app/wp-config.php:
/** The name of the database for WordPress */
define('DB_NAME', $_SERVER['DB_NAME'] ?? $_ENV['DB_NAME'] ?? null);
/** MySQL database username */
define('DB_USER', $_SERVER['DB_USER'] ?? $_ENV['DB_USER'] ?? null);
/** MySQL database password */
define('DB_PASSWORD', $_SERVER['DB_PASSWORD'] ?? $_ENV['DB_PASSWORD'] ?? null);
/** MySQL hostname */
define('DB_HOST', $_SERVER['DB_HOST'] ?? $_ENV['DB_HOST'] ?? null);
// ... later ...
define('WP_DEBUG', (bool) ($ENV['WP_DEBUG'] ?? false));
define('WP_DEBUG_LOG', (bool) ($ENV['WP_DEBUG'] ?? false));
The environment variables we set in docker-compose.yml are used here. My live server uses nginx and configures these using fastcgi_param which puts them into $_SERVER so I check that superglobal too. There’s no need to do that for WP_DEBUG as I never turn that on on live!
Conclusion
I’m not sure if this is best-practice for WordPress development, but it’s working for me so far. I particularly like that all the configuration changes that I need to make for different sites are all in docker-composer.yml which makes it reasonably easy to use this system for future projects too.
interesting article but how do you deploy to production?
Will you able to put your whole project into github for sharing?
I hope to at some point
Getting:
Building wp
ERROR: No build stage in current context
Do you know why this may be happening?
Sorry. I have no clue.
Hey Rob,just wanted to thank you for you such a thorough article. It's helped clean up my work flow quite a bit! There isn't much documentation around advanced Docker / WordPress setups
Useful, clear article. Excellent explanations and very practical config. Thanks! :)
Thanks, It helped me to install xdebug.
A bit better:
!defined('WP_DEBUG') && define('WP_DEBUG', (bool) ($_SERVER['WP_DEBUG'] ?? false));
!defined('WP_DEBUG_LOG') && define('WP_DEBUG_LOG', (bool) ($_SERVER['WP_DEBUG'] ?? false));
A bit better one more time :)
!defined('WP_DEBUG') && define('WP_DEBUG', filter_var($_SERVER['WP_DEBUG'], FILTER_VALIDATE_BOOLEAN));
!defined('WP_DEBUG_LOG') && define('WP_DEBUG_LOG', filter_var($_SERVER['WP_DEBUG_LOG'], FILTER_VALIDATE_BOOLEAN));