As a software architect, I usually “work with” technologies by drawing Boxes and Lines so I like to play with some of the more interesting stuff when I’m out of the office. Recently, I’ve worked a lot with Amazon Web Services (AWS) and Docker, having solved my initial teething problems with Docker for Windows, and I’ve decided to try something more adventurous.
I run several WordPress based web sites, including this one, but recently they seem to be running slower and slower. That’s all the excuse I need to try to run them as Docker images on AWS using the Amazon EC2 Container Service (Amazon ECS).
A quick Google shows that everyone and their dog has a blog post on this, but none of them were quite what I wanted.
So, I’ve been experimenting, one step at a time. In this article I’ll setup a WordPress site using Docker and Docker Compose that lets me develop WordPress plugins and themes as well as creating content.
Basic WordPress in Docker
If you’re following this, I’ll assume you have a reasonably up to date version of Docker installed on your computer. I use Docker for Windows, so there may be some small differences if you are using a Linux or Mac machine.
At its simplest, you can run a WordPress site using two, off the shelf Docker images:
- wordpress (https://hub.docker.com/_/wordpress/) – includes a web server, PHP extensions and installs a copy of WordPress, and
- mysql (https://hub.docker.com/_/mysql/) – a MySQL database server.
As WordPress depends on MySQL, I’ll create the database server first:
docker run -e MYSQL_ROOT_PASSWORD=rootpwd -d mysql:latest
This command downloads the latest
mysql image from Docker Hub and runs it as a demon (
-d). The root password is set using the
MYSQL_ROOT_PASSWORD environment variable (
-e). It will listen for connections on the default MySQL port, 3306.
When I run this, the output is:
D:\Projects\docker>docker run -e MYSQL_ROOT_PASSWORD=rootpwd -d mysql:latest Unable to find image 'mysql:latest' locally latest: Pulling from library/mysql ad74af05f5a2: Already exists 0639788facc8: Already exists de70fa77eb2b: Already exists 724179e94999: Already exists 50c77fb16ba6: Pull complete d51f459239fb: Pull complete 937bbdd4305a: Pull complete 35369f9634e1: Pull complete f6016aab25f1: Pull complete 5f1901e920da: Pull complete fdf808213c5b: Pull complete Digest: sha256:96edf37370df96d2a4ee1715cc5c7820a0ec6286551a927981ed50f0273d9b43 Status: Downloaded newer image for mysql:latest c3aef8b93221e003ca49f132d251d36c73dd185bd5323224324db5f3e91ee141
You can see each layer of the
mysql Docker image being pulled from Docker Hub to the local cache – though some have already been retrieved for other images (probably a common base Linux image).
Note: Patience! This might take a while for the database to initialise after starting – a couple of minutes on my aging PC.
Next, I’ll need to link WordPress to the database so I need the container name:
D:\Projects\docker>docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c3aef8b93221 mysql:latest "docker-entrypoint..." 28 seconds ago Up 26 seconds 3306/tcp happy_booth
The docker commands appear to borrow some common Unix command names. As you can see, I now have a MySQL server running in a Docker container (randomly named
happy_booth) exposing the standard port 3306 for database connections, and with a root password of ‘rootpwd’.
Next I created a basic WordPress container and connected it to the database:
docker run --link happy_booth:mysql -e WORDPRESS_DB_PASSWORD=rootpwd -p 8080:80 -d wordpress
The WordPress image runs Apache HTTPD (serving HTTP on port 80) and PHP with WordPress files under
/var/www/html. The MySQL root password is passed in with an environment variable and WordPress expects to talk to a host named mysql so I’ve linked (
happy_booth to it.
I’ve also mapped port 80 to 8080 so that I can view the WordPress site without clashing to IIS on my laptop. Navigating to http://localhost:8080 shows the start of the famous 5 minute install.
docker ps command again now shows two running containers (both with randomly generated names):
D:\Projects\docker>docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d2dd23e7dd12 wordpress "docker-entrypoint..." 9 seconds ago Up 5 seconds 0.0.0.0:8080->80/tcp angry_darwin 2c27b34003f3 mysql:latest "docker-entrypoint..." 52 seconds ago Up 50 seconds 3306/tcp happy_booth
The Name’s The Key
Those generated names are mildly amusing at first but quickly become annoying when you need to re-run the commands. So it’s best to explicitly name each container (making it easier to create a script to run them both):
docker run --name mysql -e MYSQL_ROOT_PASSWORD=rootpwd -d mysql:latest docker run --name wordpress --link mysql:mysql -p 8080:80 -e WORDPRESS_DB_PASSWORD=rootpwd -d wordpress
Also, when I first tried this, I had omitted the password from the wordpress command and spent ages working out why it worked anyway. But when I restarted the computer and tried again – it didn’t work. I think an environment variable was still set by a previous run causing this problem.
Before we move on, we have a few Docker Containers running in the background – and associated data volumes. It’s time to clean up.
First, stop all four containers:
docker stop angry_darwin happy_booth wordpress mysql
Now all those containers are stopped – but still present (use
docker ps -a to see them). So you have to remove them as well:
docker rm angry_darwin happy_booth wordpress mysql
Also, when the containers run, like any Linux O/S, they expect to have a local hard disk or equivalent. Docker creates a Volume for each of them:
D:\Projects\docker>docker volume ls DRIVER VOLUME NAME local 00586aff0096d50d6733e1b884f08bf9e85e7ccc6fc7d578401b8b8d0d2b6163 local a2f86c20f2c76e4a5791a5dd1e58d152af61c75db7a3aa67ba8b81a29030085d local cc17697d1dfb12d8f36d5654f805521c1cd713323ae0eb6d3ea13be25e8a6445 local f6839699c82ce3f789ab7cf1d77d77c1e6ee5911097137c5162baf49247f57f4
These can also be removed in a similar way (tip: in Windows, I type the command and then double click, right click and right click again to copy and paste each one to the end of the command):
D:\Projects\docker>docker volume rm f6839699c82ce3f789ab7cf1d77d77c1e6ee5911097137c5162baf49247f57f4 cc17697d1dfb12d8f36d5654f805521c1cd713323ae0eb6d3ea13be25e8a6445 a2f86c20f2c76e4a5791a5dd1e58d152af61c75db7a3aa67ba8b81a29030085d 00586aff0096d50d6733e1b884f08bf9e85e7ccc6fc7d578401b8b8d0d2b6163
You could also have restarted the containers when they were stopped (using the
docker start ... command) and the data will still be there in the volumes (if you haven’t removed them).
Before I move on, these docker commands will get more and more complex – so lets switch to Docker Compose to simplify it all.
In a suitable working directory, create a file called
# Docker Compose file to run WordPress + MySQL in 2 docker containers. version: '3' services: mysql: image: mysql:5.7 environment: - MYSQL_ROOT_PASSWORD=rootpwd wordpress: image: wordpress:latest ports: - "8080:80" environment: - WORDPRESS_DB_HOST=mysql - WORDPRESS_DB_PASSWORD=rootpwd depends_on: - mysql
With a command line in the same directory, the two containers can then be started using
D:\Projects\docker\docker_wp>docker-compose up -d Creating network "dockerwp_default" with the default driver Creating dockerwp_mysql_1 ... Creating dockerwp_mysql_1 ... done Creating dockerwp_wordpress_1 ... Creating dockerwp_wordpress_1 ... done
The docker-compose file syntax is described at https://docs.docker.com/compose/compose-file/.
docker-compose down to stop and remove the containers:
D:\Projects\docker\docker_wp>docker-compose down Stopping dockerwp_mysql_1 ... done Removing dockerwp_wordpress_1 ... done Removing dockerwp_mysql_1 ... done Removing network dockerwp_default
You can also remove any volumes (if you need) using the
-v option. However, if you are building up a site’s content, it will be stored in those volumes, so you might want to hold off deleting them till you’ve done a backup of some sort (see later articles).
That’s the basics done. In my next article (link here when I’ve written it) I’ll walk through externalising the WordPress installation and adding another developer feature.