; Date: September 23, 2020
In this post we'll discuss a simple Wordpress deployment in Docker that could be used on a production server, but is meant to be used for local MySQL development. It will use two Docker containers, one running MySQL and the other running Wordpress. It will give you full control over the Wordpress tree, and not rely on a Wordpress instance built in to a Docker image.
But first, a quick thing about Docker, since it is a popular and powerful tool for deploying services on computers as small as the Raspberry Pi up to the largest mainframes. Docker supports running Containerized applications, meaning that the application setup is distributed as a Docker Image containing a Linux instance configured with some software. We run an Image to instantiate a Docker Container, and it is the container that is where the application code executes.
To install Docker on your computer, head to
docker.com and find the instructions for your computer. For Windows and macOS there are Docker applications making it incredibly easy to get started.
For more, including an installation guide: Getting started with Docker: Installation, first steps
What we will focus on is not only getting Wordpress running on Docker, but a few hints about setting it up for development. For development we need to be able to edit files in the Wordpress tree, inspect the database, do various manipulations of the Wordpress installation, and more. It's very helpful to do all that on your local computer rather than on a remote server.
A companion article shows how to deploy Wordpress to a production server.
To proceed further, you will need both Docker and Docker Compose installed on your computer. For the Windows and Mac releases, Both are installed by installing Docker, but for Linux you'll need to install Docker Compose separately.
Launching MySQL with a Docker Compose file
The best way to learn this is to dive right in, so let's start with the simplest part, configuring a database server, MySQL, and an administrative application, PHPMyAdmin. Later we'll add in Wordpress. If you want to read a longer tutorial on this, see Set up MySQL Docker image on your laptop, then verify it works using phpMyAdmin
Not too many years ago configuring a database server required hiring a DBA (Database Administrator) with special skills, and setting aside a computer (or three) to hold the database. Today, several database engines are available simply by executing
docker run with the right arguments.
docker command has a long list of commands, and
docker run is a way to execute a Docker Image to instantiate a Container. But, in this article we'll instead do everything with a Docker Compose file.
Let's start by creating a directory to work in. For example create a blank directory named
wordpress-local. There is also a Github workspace to clone at
https://github.com/robogeek/docker-wordpress-local-development. In that directory create a file named
docker-compose.yml. As the file name implies, it is a YAML file, so it's best to use a programmers editor that knows what YAML is.
version: '3.8' services: db: image: "mysql/mysql-server:8.0.21" container_name: db command: [ "mysqld", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci", "--bind-address=0.0.0.0", "--default_authentication_plugin=mysql_native_password" ] ports: - "3306:3306" networks: - wpnet volumes: - type: bind source: ./database target: /var/lib/mysql restart: always environment: MYSQL_ROOT_PASSWORD: "w0rdw0rd" MYSQL_ROOT_HOST: "%" MYSQL_USER: dbuser MYSQL_PASSWORD: dbpassw0rd MYSQL_DATABASE: wpdb phpmyadmin: image: phpmyadmin/phpmyadmin container_name: phpmyadmin networks: - wpnet environment: PMA_HOST: db PMA_USER: root PMA_PASSWORD: w0rdw0rd PHP_UPLOAD_MAX_FILESIZE: 1G PHP_MAX_INPUT_VARS: 1G ports: - "8001:80" networks: wpnet: driver: bridge
This defines two running containers, one for the MySQL server and the other for a PHPMyAdmin instance. The latter might be useful for your database manipulation needs.
version declaration says to use the latest version of the Docker Compose specification. You'll find documentation for this file at:
services section is where we declare the containers to be run. The
networks section is where the virtual networks that will be used are declared.
The first service is named
db, hence its key in the
services section. It is built from the Docker Image named
mysql/mysql-server:8.0.21 which is pulled from Docker Hub. We are explicitly specifying MySQL version 8.0.21. For more information on this image, see:
command attribute overrides how the MySQL daemon is started. We've done this so we can specify desired MySQL configuration settings. This is a way to override whatever settings exist in the MySQL config file that's baked into the server. Another way to override the default config file, is to mount a new one into the container, but the command-line arguments support any setting we could want to change.
These settings are documented at https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html
default_authentication_plugin option forces MySQL to use the older style MySQL passwords rather than the newer
caching_sha2_password algorithm. Without this option, Wordpress will fail with the errors:
[24-Sep-2020 19:20:56 UTC] PHP Warning: mysqli_real_connect(): The server requested authentication method unknown to the client [caching_sha2_password] in /var/www/html/wp-includes/wp-db.php on line 1635 [24-Sep-2020 19:20:56 UTC] PHP Warning: mysqli_real_connect(): (HY000/2054): The server requested authentication method unknown to the client in /var/www/html/wp-includes/wp-db.php on line 1635
Even seeing this error is difficult, because Wordpress tries to be so user friendly that many errors are suppressed. The observed behavior was that running the Wordpress setup, the installer would inscrutably give an error: Error establishing a database connection. In
wp-admin/setup-config.php find the call to
error_reporting and change it to
error_reporting( -1 ). That will cause errors to be logged in
/var/log/apache2/error.log. Once you do this, the above error messages will appear.
ports attribute is how we expose TCP ports from inside a container. In this case it is exposing the standard MySQL port. What expose means is for that TCP port to be visible to the host computer. Technically this is not required, since the containers listed in the Compose file can communicate with each other without requiring the ports be visible outside of Docker. For security it is best to limit the exposed ports, since that limits the attack surface.
Since we're creating a developer-friendly deployment we may want to expose the MySQL port. For instance we might have a desktop MySQL admin tool, like MySQL Workbench, for accessing the database. In a production deployment the database port should not be exposed like this.
networks attribute specifies any Docker networks the container will use. These networks must be declared either in the Docker Compose file or via the
docker network create command. Such networks are virtual communication channels that are useful between Docker containers.
volumes attribute is how we can mount a directory, or a file, into a container. In this case we need to ensure the data stored in the database will continue existing when we destroy and recreate the container. Any data created inside a container will vaporize when the container is destroyed. Obviously the data of a database must therefore be stored outside the container.
In this case we have mounted a local directory,
./database, into the container at
/var/lib/mysql. That directory is the default location where MySQL stores its database. Therefore the
./database directory will end up holding the files of the database.
restart attribute says that if the main process in the container dies, to restart the container.
environment attribute lets us define environment variables inside the container. The MySQL container uses these environment variables while initializing the database.
MYSQL_ROOT_HOST variables control the creation of a MySQL user ID called
root which has full access to everything in the server. Since the
root account has so much power we normally limit access to it, and normally the MySQL container limits access to
root to software running inside the container. However the
MYSQL_ROOT_HOST variable lets us specify a pattern match string for which hosts are allowed to connect to the
root user. In this case the pattern,
%, says to allow connection to the
root user from any host machine anywhere.
This is not a best practice, and is flatly not what anyone should do in a production deployment. However, this is somewhat safe on our laptop, sort of.
Our purpose for doing this is to allow the PHPMyAdmin service to access the database server.
The other environment variables concern creating a database (
wpdb), a user ID (
dbuser) and that users password.
phpmyadmin section is where we set up the PHPMyAdmin service. Given what we just discussed, this should be straight-forward. The environment variables specify access particulars for the database server we just set up. And, with these settings, any access to PHPMyAdmin will automatically log in to this database.
This is another thing which is obviously not a best practice, and is not to be done in production. But for your laptop, it's kind of okay, maybe.
Notice that PHPMyAdmin is exposed on port
8001. In Docker the
ports attribute specifies a mapping from a port number inside the container, to the port number it will appear on outside the container. In this case PHPMyAdmin by default runs on port
80 but we want to leave that for Wordpress. Therefore we've mapped it to port
Starting the database container, and kicking the tires
With the Docker Compose file in hand, we simply run the
docker-compose command to launch what's in the file.
$ docker-compose up phpmyadmin is up-to-date Creating db ... done Attaching to phpmyadmin, db db | [Entrypoint] MySQL Docker Image 8.0.21-1.1.17 phpmyadmin | AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.27.0.2. Set the 'ServerName' directive globally to suppress this message phpmyadmin | AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.27.0.2. Set the 'ServerName' directive globally to suppress this message phpmyadmin | [Thu Sep 24 02:40:03.673959 2020] [mpm_prefork:notice] [pid 1] AH00163: Apache/2.4.38 (Debian) PHP/7.4.9 configured -- resuming normal operations phpmyadmin | [Thu Sep 24 02:40:03.674075 2020] [core:notice] [pid 1] AH00094: Command line: 'apache2 -D FOREGROUND' db | [Entrypoint] Initializing database ...
There's a LOT more output to come due to what the MySQL container does to initialize the data directory. But, launching the database is that simple. Unless, that is, you instead get an error message like this:
$ docker-compose up Creating network "wordpress-local_wpnet" with driver "bridge" Creating phpmyadmin ... Creating db ... error Creating phpmyadmin ... done: bind source path does not exist: /Volumes/Extra/docker/wordpress-local/database ERROR: for db Cannot create container for service db: invalid mount config for type "bind": bind source path does not exist: /Volumes/Extra/docker/wordpress-local/database ERROR: Encountered errors while bringing up the project.
As error messages from MySQL go this one is fairly easy to understand. Maybe that's because it's not a MySQL error, but a Docker error. In any case the message means the directory we named in the Compose File does not exist. If that's the case simply run this:
$ mkdir database
That will create an empty directory and the MySQL container will proceed to initialize database files in that directory.
There is another consideration regarding this directory. Suppose you want to try different parameters when creating the database directory? If the MySQL container see's a data directory when it launches, it will not initialize the data directory because obviously it shouldn't initialize a directory which already contains database files. But if you're experimenting with new settings, you need the data directory to be initialized. Therefore, if that's what you need, then you must first delete the data directory, and create an empty directory, so that the MySQL container will go ahead and do its initialization.
The next thing is to inspect what's been created:
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 61e96b1d0a27 mysql/mysql-server:8.0.21 "/entrypoint.sh mysq…" About a minute ago Up About a minute (healthy) 0.0.0.0:3306->3306/tcp, 33060/tcp db c0f5868f3f2b phpmyadmin/phpmyadmin "/docker-entrypoint.…" About a minute ago Up About a minute 0.0.0.0:8001->80/tcp wordpress-local_phpmyadmin_1
docker ps command lists the running containers, and we see our two containers are running. It shows the port mappings that were declared in the Compose file. And the last column shows you the name for the container.
The next thing is to launch a command shell inside the container:
docker exec -it db bash
exec command executes a process inside the container, and adding the
-it flag executes that process on an interactive terminal. The
db argument is the container name. The last part of the command line is where we specify the command to execute, in this case
bash so that we get a command shell.
Once inside the container we can run this:
bash-4.2# mysql -u root -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 11 Server version: 8.0.21 MySQL Community Server - GPL
This is executed inside the container. We launch the
mysql CLI tool, telling it to login with the
root user. After specifying the password we end up at the MySQL command prompt.
mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | sys | | wpdb | +--------------------+ 5 rows in set (0.02 sec) mysql>
We have a whole world of SQL commands we can type at this prompt. With this one we can see that our
wpdb database has been created.
Another useful thing to do is to visit
http://localhost:8001 to use PHPMyAdmin to inspect the database.
With this we've proved we have a functioning database server. What's next is to set up the Wordpress container.
To shut down the database service, run:
Launching a Docker container for executing Wordpress
So far we've set up a database layer useful for software development. We have both a MySQL server and a powerful administrative tool, PHPMyAdmin. Wordpress uses MySQL to store content, and our next step in this project is to configure a Docker container to host a Wordpress instance, and tell that instance to use the database we created.
The first step is to download a Wordpress tarball from https://wordpress.org/download/
The Wordpress project offers both ZIP and
.tar.gz bundles, so download whichever one you are comfortable with using. Then unpack that bundle and note the directory that is created, probably
Then execute these commands:
$ mkdir roots $ mv wordpress roots/html $ chmod -R 33:33 roots/html $ mkdir logs $ mkdir mybin
This is pretty straight-forward except for the
chmod command. What this does is to ensure the files in that hierarchy are owned by the
www-data user ID and group ID. Your laptop may have a
www-data user ID, but the one we want to use is whatever is configured inside the container. After the container is running, you can start a command shell inside the container and inspect the value like so:
root@2bc6acb6ddbc:/var/www/html# id www-data uid=33(www-data) gid=33(www-data) groups=33(www-data)
The Apache process is configured to change it's user and group ID to these values. For the Wordpress instance to function, the files must be owned by this user ID.
These steps prepare us for the Wordpress container definition. To understand why, let's take a look at it.
docker-compose.yml, add this section:
wp: image: wordpress:php7.2-apache container_name: wp networks: - wpnet restart: always ports: - '4080:80' environment: PATH: "/usr/local/sbin:/usr/local/mybin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" volumes: - ./logs:/var/log/apache2:rw - ./roots/html:/var/www/html:rw # - ./sites-enabled:/etc/apache2/sites-enabled:rw - ./mybin:/usr/local/mybin
On Docker Hub you'll find there is a Docker image named
wordpress. The particular variant chosen here is configured to have PHP v7.2 installed, and to use Apache as the web server. Feel free to select a different PHP version. But for the purpose of this project, keep the
-apache portion. This tutorial is based around using Apache because that gives the quickest route to launching Wordpress in a development environment.
Most of this should be straight-forward to understand. For example, we've mapped the HTTP port so it appears on port
Setting the PATH environment variable is set so we can add our own commands to run in the container. The most interesting is to install the WP-CLI command-line tool available, since it is so useful for Wordpress development. To do that, run these commands:
$ curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar $ chmod +x wp-cli.phar $ mv wp-cli.phar ./mybin/wp
Once you do that, you'll be able to run the
wp command inside the container. That tool has a long list of capabilities to manipulate a Wordpress installation.
logs directory is mounted to
/var/log/apache2 to make it easy to view the logs.
roots/html directory is mounted as
/var/www/html. If you executed the commands shown above, that directory is where we unpacked the Wordpress bundle.
The Wordpress container has Apache configured with a default virtual host based on the
/var/www/html directory. When this container launches, it looks for a Wordpress installation in that directory. If there is no Wordpress installed there, it will copy a set of pristine Wordpress sources to that directory. But what we've done is pre-populated
roots/html with the Wordpress distribution, and mounted it to
/var/www/html. Those two steps ensure the Wordpress container will not install Wordpress for us, but instead it will use the Wordpress files we installed.
What that means is we are fully in control of the content of the Wordpress code being used. For example if we're developing a custom theme, or custom plugin, we can directly plop the source into
roots/html/wp-content because we're in control. Further we can edit any other file in that directory hierarchy, because those steps ensured we're in control.
There is an option here, commented out, which is to override the built-in
sites-enabled directory. This directory is used by Apache to hold configuration files for virtual hosts. The Wordpress container has a default config file in that directory which is good enough for us. But you may want to override this behavior, to change the config file, or to have multiple virtual hosts. If that's what you want to do, the first step is to mount this directory, and then to put your desired configuration files there. What you do beyond that is up to you.
Starting the Wordpress container, and kicking the tires
We have everything set up to launch Wordpress along with the MySQL instance we launched earlier. Since we've added another service to the Compose file, we need to relaunch the system.
If you haven't already done so, make sure the system is shut down. As noted earlier you can run
docker-compose down, and the services will stop.
You then run:
docker-compose up to restart the system.
After it starts up again, rerun this:
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2bc6acb6ddbc wordpress:php7.2-apache "docker-entrypoint.s…" 4 hours ago Up 4 hours 0.0.0.0:4080->80/tcp wp dc266ca880c0 mysql/mysql-server:8.0.21 "/entrypoint.sh mysq…" 4 hours ago Up 4 hours (healthy) 0.0.0.0:3306->3306/tcp, 33060/tcp db 764f4a301ccd phpmyadmin/phpmyadmin "/docker-entrypoint.…" 4 hours ago Up 4 hours 0.0.0.0:8001->80/tcp phpmyadmin
You'll see a
wp container running alongside the
phpmyadmin containers. The other thing to notice is port 4080 is exposed, just like it says in the Compose file.
The moment of truth is to open
http://localhost:4080 in your browser. With the current Wordpress setup experience, you'll first be listed with a set of languages to use, then you'll see this explanation:
This explains that Wordpress needs to know your database particulars.
This is where we enter the database particulars. The values are what we entered in the Docker Compose file.
Ideally when you click the Submit button you'll be shown messages about the database tables being set up and so on. However it is possible you'll instead see an error screen headed with the message Error establishing a database connection.
Earlier in this post we talked about one rather inscrutable cause for that error. We already went over how to avoid that error. The more common reason for getting that error is if the database particulars were wrong. This is where you carefully double-check the values you entered to ensure they're all correct.
Another common issue is that the database is unreachable. If your Compose file matches what was shown above that won't be the case. But let's discuss how to debug the database connection anyway.
First task is to use PHPMyAdmin and inspect the database server.
The second is to start a command shell inside the Wordpress container:
docker exec -it wp bash
This starts a command shell, and one thing we can do right away is this:
root@2bc6acb6ddbc:/var/www/html# wp --allow-root --info OS: Linux 4.19.76-linuxkit #1 SMP Tue May 26 11:42:35 UTC 2020 x86_64 Shell: PHP binary: /usr/local/bin/php PHP version: 7.2.33 php.ini used: WP-CLI root dir: phar://wp-cli.phar/vendor/wp-cli/wp-cli WP-CLI vendor dir: phar://wp-cli.phar/vendor WP_CLI phar path: /var/www/html WP-CLI packages dir: WP-CLI global config: WP-CLI project config: WP-CLI version: 2.4.0
This of course requires that you've installed WP-CLI. This particular command simply tells you a bit of status information. What we really need to do is verify that the Wordpress container can access the database in the MySQL container.
In theory we would simply do this:
root@2bc6acb6ddbc:/var/www/html# wp --allow-root db cli /usr/bin/env: 'mysql': No such file or directory
But as you can see, it relies on the
mysql command which isn't installed. Fortunately we can easily remedy this by installing a Debian package.
root Tables_in_wpdb | +-----------------------+ | wp_commentmeta | | wp_comments | | wp_links | | wp_options | | wp_postmeta | | wp_posts | | wp_term_relationships | | wp_term_taxonomy | | wp_termmeta | | wp_terms | | wp_usermeta | | wp_users | +-----------------------+ 12 rows in set (0.004 sec) MySQL [wpdb]>apt-get update ... root apt-get install mariadb-client ... root wp --allow-root db cli Welcome to the MariaDB monitor. Commands end with ; or \g. Your MySQL connection id is 576 Server version: 8.0.21 MySQL Community Server - GPL Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. MySQL [wpdb]> show tables; +-----------------------+ |
The Wordpress container is derived from Debian Linux, so therefore we have access to their large library of packages. There is not a
mysql-client package, but the MariaDB database is plug-and-play compatible with MySQL. Therefore the
mariadb-client package provides a compatible version of the
mysql command installed, we can verify access to the database using the user ID and password we configured.
Another test is to use
ping to see if packets can reach the
root@2bc6acb6ddbc:/var/www/html# ping db bash: ping: command not found root@2bc6acb6ddbc:/var/www/html# apt-get install inetutils-ping ... root@2bc6acb6ddbc:/var/www/html# ping db PING db (192.168.16.3): 56 data bytes 64 bytes from 192.168.16.3: icmp_seq=0 ttl=64 time=1.266 ms 64 bytes from 192.168.16.3: icmp_seq=1 ttl=64 time=0.371 ms ...
mysql command, this package is not installed by default, but is easy to bring in. Once installed, we can ping the database container and see that indeed it is there.
Once you've successfully gotten Wordpress to install itself, we'll see this screen:
This does some final setup, after which you'll be brought to the login form. After logging in, you'll be in the Wordpress dashboard area. If you've read this far, you probably already know what to do in the Wordpress dashboard.
Given that you probably know what to do with Wordpress, let's instead talk about how to shut down the system and remove all traces.
Shutting down the Wordpress installation, deleting the database
We've already seen how to stop the deployed services:
$ docker-compose down Stopping wp ... done Stopping db ... done Stopping phpmyadmin ... done Removing wp ... done Removing db ... done Removing phpmyadmin ... done Removing network wordpress-local_wpnet
This stops and removes the containers, and even removes the virtual network which was created.
That's easy, but there's still a database and other things taking up disk space.
$ du -sk * 195032 database 4 docker-compose.yml 13472 latest.tar.gz 52 logs 5880 mybin 51160 roots 4 sites-enabled
In other words, if you want to dispose of the database or other things then delete them.