Docker Compose makes dealing with the orchestration processes of Docker containers (such as starting up, shutting down, and setting up intra-container linking and volumes) really easy.
This article provides a real-world example of using Docker Compose to install an application, in this case WordPress with PHPMyAdmin as an extra. WordPress normally runs on a LAMP stack, which means Linux, Apache, MySQL/MariaDB, and PHP. The official WordPress Docker image includes Apache and PHP for us, so the only part we have to worry about is MariaDB.
To follow this article, you will need the following:
We’ll be using the official WordPress and MariaDB Docker images. If you’re curious, there’s lots more info about these images and their configuration options on their respective GitHub and Docker Hub pages.
Let’s start by making a folder where our data will live and creating a minimal docker-compose.yml
file to run our WordPress container:
mkdir ~/wordpress && cd $_
Then create a ~/wordpress/docker-compose.yml
with your favorite text editor (nano is easy if you don’t have a preference):
- nano ~/wordpress/docker-compose.yml
and paste in the following:
wordpress:
image: wordpress
This just tells Docker Compose to start a new container called wordpress
and download the wordpress
image from the Docker Hub.
We can bring the image up like so:
- docker-compose up
You’ll see Docker download and extract the WordPress image from the Docker Hub, and after some time you’ll get some error messages similar to the below:
Outputwordpress_1 | error: missing WORDPRESS_DB_HOST and MYSQL_PORT_3306_TCP environment variables
wordpress_1 | Did you forget to --link some_mysql_container:mysql or set an external db
wordpress_1 | with -e WORDPRESS_DB_HOST=hostname:port?
dockercompose_wordpress_1 exited with code 1
This is WordPress complaining that it can’t find a database. Let’s add a MariaDB image to the mix and link it up to fix that.
To add the MariaDB image to the group, re-open docker-compose.yml
with your text editor:
nano ~/wordpress/docker-compose.yml
Change docker-compose.yml
to match the below (be careful with the indentation, YAML files are white-space sensitive)
wordpress:
image: wordpress
links:
- wordpress_db:mysql
wordpress_db:
image: mariadb
What we’ve done here is define a new container called wordpress_db
and told it to use the mariadb
image from the Docker Hub. We also told the our wordpress
container to link our wordpress_db
container into the wordpress
container and call it mysql
(inside the wordpress
container the hostname mysql
will be forwarded to our wordpress_db
container).
If you run docker-compose up
again, you will see it download the MariaDB image, and you’ll also see that we’re not quite there yet though:
Outputwordpress_db_1 | error: database is uninitialized and MYSQL_ROOT_PASSWORD not set
wordpress_db_1 | Did you forget to add -e MYSQL_ROOT_PASSWORD=... ?
wordpress_1 | error: missing required WORDPRESS_DB_PASSWORD environment variable
wordpress_1 | Did you forget to -e WORDPRESS_DB_PASSWORD=... ?
wordpress_1 |
wordpress_1 | (Also of interest might be WORDPRESS_DB_USER and WORDPRESS_DB_NAME.)
wordpress_wordpress_db_1 exited with code 1
wordpress_wordpress_1 exited with code 1
Gracefully stopping... (press Ctrl+C again to force)
WordPress is still complaining about being unable to find a database, and now we have a new complaint from MariaDB saying that no root password is set.
It appears that just linking the two containers isn’t quite enough. Let’s go ahead and set the MYSQL_ROOT_PASSWORD
variable so that we can actually fire this thing up.
Edit the Docker Compose file yet again:
- nano ~/wordpress/docker-compose.yml
Add these two lines to the end of the wordpress_db
section, but make sure to change examplepass
to a more secure password!
wordpress_db:
...
environment:
MYSQL_ROOT_PASSWORD: examplepass
...
This will set an environment variable inside the wordpress_db
container called MYSQL_ROOT_PASSWORD
with your desired password. The MariaDB Docker image is configured to check for this environment variable when it starts up and will take care of setting up the DB with a root account with the password defined as MYSQL_ROOT_PASSWORD
.
While we’re at it, let’s also set up a port forward so that we can connect to our WordPress install once it actually loads up. Under the wordpress
section add these two lines:
wordpress:
...
ports:
- 8080:80
...
The first port number is the port number on the host, and the second port number is the port inside the container. So, this configuration forwards requests on port 8080 of the host to the default web server port 80 inside the container.
Note: If you would like Wordpress to run on the default web server port 80 on the host, change the previous line to 80:80
so that requests to port 80 on the host are forwarded to port 80 inside the Wordpress container.
Your complete docker-compose.yml
file should now look like this:
wordpress:
image: wordpress
links:
- wordpress_db:mysql
ports:
- 8080:80
wordpress_db:
image: mariadb
environment:
MYSQL_ROOT_PASSWORD: examplepass
With this configuration we can actually go ahead and fire up WordPress. This time, let’s run it with the -d
option, which will tell docker-compose
to run the containers in the background so that you can keep using your terminal:
- docker-compose up -d
You’ll see a whole bunch of text fly by your screen. Once it’s calmed down, open up a web browser and browse to the IP of your DigitalOcean box on port 8080 (for example, if the IP address of your server is 123.456.789.123 you should type http://123.456.789.123:8080 into your browser.)
You should see a fresh WordPress installation page and be able to complete the install and blog as usual.
Because these are both official Docker images and are following all of Docker’s best practices, each of these images have pre-defined, persistent volumes for you — meaning that if you restart the container, your blog posts will still be there. You can learn more about working with Docker volumes in the Docker data volumes tutorial.
Great, that was relatively painless. Let’s try getting a little fancy.
So far we’ve only been using official images, which the Docker team takes great pains to ensure are accurate. You may have noticed that we didn’t have to give the WordPress container any environment variables to configure it. As soon as we linked it up to a properly configured MariaDB container everything just worked.
This is because there’s a script inside the WordPress Docker container that actually grabs the MYSQL_ROOT_PASSWORD
variable from our wordpress_db
container and uses that to connect to WordPress.
Let’s venture out of the official image area a little bit and use a community contributed PhpMyAdmin image. Go ahead and edit docker-compose.yml
one more time:
- nano docker-compose.yml
Paste the following at the end of the file:
phpmyadmin:
image: corbinu/docker-phpmyadmin
links:
- wordpress_db:mysql
ports:
- 8181:80
environment:
MYSQL_USERNAME: root
MYSQL_ROOT_PASSWORD: examplepass
Be sure to replace examplepass with the exact same root password from the wordpress_db
container you setup earlier.
This grabs docker-phpmyadmin
by community member corbinu
, links it to our wordpress_db
container with the name mysql
(meaning from inside the phpmyadmin
container references to the hostname mysql
will be forwarded to our wordpress_db
container), exposes its port 80 on port 8181 of the host system, and finally sets a couple of environment variables with our MariaDB username and password. This image does not automatically grab the MYSQL_ROOT_PASSWORD
environment variable from the wordpress_db
container’s environment the way the wordpress
image does. We actually have to copy the MYSQL_ROOT_PASSWORD: examplepass
line from the wordpress_db
container, and set the username to root
.
The complete docker-compose.yml
file should now look like this:
wordpress:
image: wordpress
links:
- wordpress_db:mysql
ports:
- 8080:80
wordpress_db:
image: mariadb
environment:
MYSQL_ROOT_PASSWORD: examplepass
phpmyadmin:
image: corbinu/docker-phpmyadmin
links:
- wordpress_db:mysql
ports:
- 8181:80
environment:
MYSQL_USERNAME: root
MYSQL_ROOT_PASSWORD: examplepass
Now start up the application group again:
- docker-compose up -d
You will see PhpMyAdmin being installed. Once it is finished, visit your server’s IP address again (this time using port 8181, e.g. http://123.456.789.123:8181). You’ll be greeted by the PhpMyAdmin login screen.
Go ahead and login using username root
and password you set in the YAML file, and you’ll be able to browse your database. You’ll notice that the server includes a wordpress
database, which contains all the data from your WordPress install.
You can add as many containers as you like this way and link them all up in any way you please. As you can see, the approach is quite powerful —instead of dealing with the configuration and prerequisites for each individual components and setting them all up on the same server, you get to plug the pieces together like Lego blocks and add components piecemeal. Using tools like Docker Swarm you can even transparently run these containers over multiple servers! That’s a bitoutside the scope of this tutorial though. Docker provides some documentation on it if you are interested.
Since all the files for your new WordPress site are stored inside your Docker container, what happens to your files when you stop the container and start it again?
By default, the document root for the WordPress container is persistent. This is because the WordPress image from the Docker Hub is configured this way. If you make a change to your WordPress site, stop the application group, and start it again, your website will still have the changes you made.
Let’s try it.
Go to your WordPress from a web browser (e.g. http://123.456.789.123:8080). Edit the Hello World! post that already exists. Then, stop all the Docker containers with the following command:
- docker-compose stop
Try loading the WordPress site again. You will see that the website is down. Start the Docker containers again:
- docker-compose up -d
Again, load the WordPress site. You should see your blog site and the change you made earlier. This shows that the changes you make are saved even when the containers are stopped.
It is possible to store the document root for WordPress on the host filesystem using a Docker data volume to share files between the host and the container.
Note: For more details on working with Docker data volumes, take a look at the Docker data volumes tutorial.
Let’s give it a try. Open up your docker-compose.yml
file one more time:
nano ~/wordpress/docker-compose.yml
in the wordpress:
section add the following lines:
wordpress:
...
volumes:
- ~/wordpress/wp_html:/var/www/html
...
Stop your currently running docker-compose
session:
- docker-compose stop
Remove the existing container so we can map the volume to the host filesystem:
- docker-compose rm wordpress
Start WordPress again:
- docker-compose -d
Once the prompt returns, WordPress should be up and running again — this time using the host filesystem to store the document root.
If you look in your ~/wordpress
directory, you’ll see that there is now a wp_html
directory in it:
- ls ~/wordpress
All of the WordPress source files are inside it. Changes you make will be picked up by the WordPress container in real time.
This experience was a little smoother than it normally would be — the WordPress Docker container is configured to check if /var/www/html
is empty or not when it starts and copies files there appropriately. Usually you will have to do this step yourself.
You should have a full WordPress deploy up and running. You should be able to use the same method to deploy quite a wide variety of systems using the images available on the Docker Hub. Be sure to figure out which volumes are persistent and which are not for each container you create.
Happy Dockering!
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Just a small typo in: Start WordPress again:
Once the prompt returns, WordPress should be up and running again — this time using the host filesystem to store the document root.
For the mariadb/mysql section I wanted to preserve the database files, so the following volume pattern for the wordpress_db section worked for me:
This maps the host directory /somehostpath/dbsubdirname to the standard location within the container for mariadb/mysql database directory. The phpMyAdmin worked as expected.
How do I php.ini config to increase the upload limit?
add virtual host config (domain.com.conf) to the wordpress docker
While this solution works great, i randomly get the “Error establishing a database connection”, after a while that the container is up. The db container stops and i have to run the docker-compose.yml file again to bring the db up.
Any ideas why this common wordpress error happens here?
Nice article, where exactly the wp core file store? I can’t add a new theme.
Thanks
Hello corbinu/docker-phpmyadmin is deprecated, --link option is also. Could we get updated post from digital ocean?? Thanks!!
First, thank you Tammy Fox for this article, it helps me so much) Can someone explain how to create systemd unit for docker compose? I mean, I create one with following contents:
It successfully starts all containers, but, when I manually stop one of containers it won’t start it again. How can I do that? I want that systemd unit watches all containers and starts them automatically (then one of them fail).
I try to update the compose file with v2 version
I can connect to phpmyadmin but I cannot connect to WP.
Any idea ?
Hello,
I’ve got this error when executing docker-compose up
Traceback (most recent call last): File “/usr/bin/docker-compose”, line 5, in <module> from pkg_resources import load_entry_point File “/usr/lib/python2.7/site-packages/pkg_resources.py”, line 3011, in <module> parse_requirements(requires), Environment() File “/usr/lib/python2.7/site-packages/pkg_resources.py”, line 626, in resolve raise DistributionNotFound(req) pkg_resources.DistributionNotFound: backports.ssl-match-hostname>=3.5