Wordpress local development environment with Docker on your laptop

; Date: September 23, 2020

Tags: Docker »»»» Docker MAMP »»»» Docker Wordpress »»»» Wordpress

We normally install Wordpress on a public Internet server, and installation on your local machine is normally limited to developing or testing a Wordpress feature or theme. The development loop is a lot shorter when you can directly edit Wordpress files on your machine, as opposed to working out a method to edit remote files. While it's convenient, it's not always clear how to create a website hosting environment on your laptop. In the past we would use MAMP (or the Windows equivalent), which is a macOS-native environment for running Apache/MySQL/PHP stacks. But today we have a different tool, Docker, that is easily able to run that stack, as well as any other stack. Docker easily runs on a laptop, so let's take a look at using it to run a local Wordpress development environment.

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.

The 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 (github.com) 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.

The version declaration says to use the latest version of the Docker Compose specification. You'll find documentation for this file at: (docs.docker.com) https://docs.docker.com/compose/compose-file/

The 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: (hub.docker.com) https://hub.docker.com/r/mysql/mysql-server

The 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 (dev.mysql.com) https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html

The 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.

The 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.

The 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.

The 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.

The restart attribute says that if the main process in the container dies, to restart the container.

The environment attribute lets us define environment variables inside the container. The MySQL container uses these environment variables while initializing the database.

The MYSQL_ROOT_PASSWORD and 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.

The 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 8001.

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

The 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

The 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: docker-compose down

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 (wordpress.org) 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 wordpress.

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.

Next, in 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 4080.

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.

The logs directory is mounted to /var/log/apache2 to make it easy to view the logs.

The 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 db and 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@2bc6acb6ddbc:/var/www/html# apt-get update
...
root@2bc6acb6ddbc:/var/www/html# apt-get install mariadb-client
...
root@2bc6acb6ddbc:/var/www/html# 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;
+-----------------------+
| 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]> 

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 tool.

With 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 db container:

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
...

Like the 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.

About the Author(s)

David Herron : David Herron is a writer and software engineer focusing on the wise use of technology. He is especially interested in clean energy technologies like solar power, wind power, and electric cars. David worked for nearly 30 years in Silicon Valley on software ranging from electronic mail systems, to video streaming, to the Java programming language, and has published several books on Node.js programming and electric vehicles.