Docker and docker-compose for WordPress testing - 01

The Docker promise, in a WordPress developer context

Docker is almost everywhere now, and there are many good reasons for it.
I'm not going into all the reasons that make Docker amazing. Instead, I would focus on the possibilities it offers me, as a WordPress developer sharing production and test code across different teams and operating systems.
"It works on my machine" might be a meme, but it's still frequent feedback I run into when reviewing pull requests with broken tests that are, invariably, passing locally and failing in CI. Supposedly.
It would be great for a team, or even just for a developer using multiple operating systems, to have identical testing environments running on each developer machine, whatever the operating system.

The purpose of this series is to consolidate the knowledge I got along the way of learning "how to docker" into a progressive series of articles moving past base, copy-and-paste examples. Ideally, to set up to an OS-independent, reusable, flexible docker-compose based, WordPress local, and CI (Continuous Integration) testing and development environment.

Up and done

I'm using, to start, a container I use daily: the official WordPress container from Dockerhub.

This container is a constant in my builds and testing environments. I've spent quite a bit of time learning it's ins-and-outs and collecting several "gotchas along the path.

I've created a minimal docker-compose.yml configuration file copying the example from the dockerhub official WordPress image documentation:

cd ~/Repos
mkdir wp-docker
cd wp-docker
touch docker-compose.yml

I've just moved around some lines but changed nothing.

version: '3.1'

volumes:

  wordpress:
  db:

services:

  wordpress:
    image: wordpress
    restart: always
    ports:
      # Expose port 80 of the container on port 8080 of localhost.
      - 8080:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
      WORDPRESS_DB_NAME: exampledb
    volumes:
      - wordpress:/var/www/html

  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: exampledb
      MYSQL_USER: exampleuser
      MYSQL_PASSWORD: examplepass
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - db:/var/lib/mysql

I run the command to start the stack and wait for the setup to be ready:

docker-compose up

If the images are not already present on my machine, they will be downloaded and extracted first, and this will happen only once the first time I require new images.

After a bit of setup and build processes, I will be left with a terminal tab or window following (tailing) the two containers log files:

db_1         | 2020-05-16T11:24:01.540475Z 0 [Note] Event Scheduler: Loaded 0 events
db_1         | 2020-05-16T11:24:01.540824Z 0 [Note] mysqld: ready for connections.
db_1         | Version: '5.7.29'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
wordpress_1  | AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 192.168.80.3. Set the 'ServerName' directive globally to suppress this message
wordpress_1  | AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 192.168.80.3. Set the 'ServerName' directive globally to suppress this message
wordpress_1  | [Sat May 16 11:24:03.714651 2020] [mpm_prefork:notice] [pid 1] AH00163: Apache/2.4.38 (Debian) PHP/7.3.16 configured -- resuming normal operations
wordpress_1  | [Sat May 16 11:24:03.714774 2020] [core:notice] [pid 1] AH00094: Command line: 'apache2 -D FOREGROUND'

In the docker-compose.yml file I've specified, in services.wordpress.ports section, that I would like to expose port 80 of the container on port 8080 of localhost.

Up and down and up again

When I open the http://localhost:8080 address in my browser, though, WordPress will not be installed.

WordPress requires installation on first run

I go through the installation and will end up with a working WordPress installation:

WordPress installed

If I stop the WordPress and database server closing the terminal tab running the logs or sending it the termination command (Ctrl+c), then the site will not be available anymore at http://localhost:8080, as expected.

Stopping the stack from the terminal

How would I restart the WordPress installation to work on it, though? Using the same command I've used before:

docker-compose up

Do I need to install WordPress again? No.
In the docker-compose.yml file I've defined two volumes: wordpress and db.
The wordpress volume will store, in my host machine filesystem, the WordPress container files, the content of the WordPress installation.
The db volume will store the data produced by the database instance used by WordPress, again in my host machine filestystem.

The concept of volumes and "Docker volumes" was tricky for me to grasp until I've understood that it just means "store what files you create or update during your work on my machine".

So where are the files?

If they are on my machine, then I would expect the files to be somewhere I can see them; in the same directory containing the docker-compose.yml file, ideally.

Yet they are not there, the directory contains only, even while the wordpress and db containers are running, the docker-compose.yml file:

Repos/wp-docker » tree -L 1 .
.
└── docker-compose.yml

0 directories, 1 file

Getting the path to the real location where, in the host filesystem, the files live requires a bit of inspection into how Docker stores container information.

First, I list the currently defined WordPress containers with docker volume ls:

Repos/wp-docker » docker volume ls
DRIVER              VOLUME NAME
local               wp-docker_db
local               wp-docker_wordpress

As expected, Docker created two volumes have named after the directory where the docker-compose.yml file lives:

  1. wp-docker_wordpress for the wordpress volume
  2. wp-docker_db for the db volume

Since I want to see the WordPress container files, the container I'm interested in is the wp-docker_wordpress one.
The command docker volume inspect wp-docker_wordpress will provide me with useful information about the volume:

Repos/wp-docker » docker volume inspect wp-docker_wordpress
[
    {
        "CreatedAt": "2020-05-16T11:46:15Z",
        "Driver": "local",
        "Labels": {
            "com.docker.compose.project": "wp-docker",
            "com.docker.compose.version": "1.25.5",
            "com.docker.compose.volume": "wordpress"
        },
        "Mountpoint": "/var/lib/docker/volumes/wp-docker_wordpress/_data",
        "Name": "wp-docker_wordpress",
        "Options": null,
        "Scope": "local"
    }
]

So, are the wordpress volume files, used by the wordpress container, from the wp-docker project, in the /var/lib/docker/volumes/wp-docker_wordpress/_data directory?
Turns out no, they are not:

Repos/wp-docker » ls -la /var/lib/docker/volumes/wp-docker_wordpress/_data
ls: /var/lib/docker/volumes/wp-docker_wordpress/_data: No such file or directory

I could go into the rabbit hole of trying and finding them, but Docker and docker-compose provide a structured and documented way to store the container data exactly where I need it.

Binding volumes

Binding volumes is container jargon to say:

When I put stuff in this directory on my machine, it should appear in this directory in the container. Possibly the other way too.

I do want the WordPress installation files to exist, on my host machine, somewhere practical.
Specifically in the ~/Repos/wp-docker/_wordpress directory.

I update the docker-compose.yml file to reflect that.

version: '3.1'

volumes:

  db:

services:

  wordpress:
    image: wordpress
    restart: always
    ports:
      - 8080:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
      WORDPRESS_DB_NAME: exampledb
    volumes:
      # Not using the volume defined in the volumes section anymore, so I've removed it.
      # The `.` means "from the directory that contains this file", where "this file" is the docker-compose.yml file.
      - ./_wordpress:/var/www/html

  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: exampledb
      MYSQL_USER: exampleuser
      MYSQL_PASSWORD: examplepass
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - db:/var/lib/mysql

Two things to notice:

I've changed the services.wordpress.volumes entry to ./_wordpress:/var/www/html. The . in the ./_wordpress first fragment of the wordpress service volume definition means "from the directory that contains this file", where "this file" is the docker-compose.yml file.
In docker-compose.yml files, as in the -v parameter of the docker CLI API, volume bindings are read on_the_host:on_the_container. The same applies to port bindings. The first, the host part of a volume binding can either be a path relative to the docker-compose.yml directory, or an absolute path. I'm using the first option here to make the implementation portable.

Since I will not be using the wordpress volume anymore, I've removed it from the file.

If all goes well, firing up the stack again should create a _wordpress directory beside the docker-compose.yml file and put WordPress root directory in there.

I see WordPress

I fire up the stack again and wait, looking at the logs, for the message from the WordPress container to confirm it's ready.

Repos/wp-docker » docker-compose up -d

I open a new terminal tab and check what's in the project directory:

Repos/wp-docker » tree -L 1 .
.
└── docker-compose.yml

0 directories, 1 file

Nothing happened. Why?

Well: Docker is an efficient worker and, since I ran the stack before, will "recycle", reuse, the containers I was running the last time.
And the wordpress container I used before was never told to bind the /var/www/html directory to the _wordpress one in the project root directory. So it didn't.

How do I take control of this back? By tearing down the stack and firing it up again with the down (short for "tear down") command:

Repos/wp-docker » docker-compose down
Stopping wp-docker_wordpress_1 ... done
Stopping wp-docker_db_1        ... done
Removing wp-docker_wordpress_1 ... done
Removing wp-docker_db_1        ... done
Removing network wp-docker_default

Repos/wp-docker » docker-compose up
Creating network "wp-docker_default" with the default driver
Creating wp-docker_wordpress_1 ... done
Creating wp-docker_db_1        ... done
Attaching to wp-docker_db_1, wp-docker_wordpress_1
db_1         | 2020-05-17 11:16:48+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.29-1debian9 started.
db_1         | 2020-05-17 11:16:49+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
db_1         | 2020-05-17 11:16:49+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.29-1debian9 started.
db_1         | 2020-05-17T11:16:49.337125Z 0 [Warning

...

db_1         | Version: '5.7.29'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
wordpress_1  | WordPress not found in /var/www/html - copying now...
wordpress_1  | Complete! WordPress has been successfully copied to /var/www/html
wordpress_1  | AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 192.168.96.3. Set the 'ServerName' directive globally to suppress this message
wordpress_1  | AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 192.168.96.3. Set the 'ServerName' directive globally to suppress this message
wordpress_1  | [Sun May 17 11:16:59.711493 2020] [mpm_prefork:notice] [pid 1] AH00163: Apache/2.4.38 (Debian) PHP/7.3.16 configured -- resuming normal operations
wordpress_1  | [Sun May 17 11:16:59.711569 2020] [core:notice] [pid 1] AH00094: Command line: 'apache2 -D FOREGROUND'

Can I see WordPress files in the _wordpress directory now?!

Repos/wp-docker » tree -L 2 .
.
├── _wordpress
│   ├── index.php
│   ├── license.txt
│   ├── readme.html
│   ├── wp-activate.php
│   ├── wp-admin
│   ├── wp-blog-header.php
│   ├── wp-comments-post.php
│   ├── wp-config-sample.php
│   ├── wp-config.php
│   ├── wp-content
│   ├── wp-cron.php
│   ├── wp-includes
│   ├── wp-links-opml.php
│   ├── wp-load.php
│   ├── wp-login.php
│   ├── wp-mail.php
│   ├── wp-settings.php
│   ├── wp-signup.php
│   ├── wp-trackback.php
│   └── xmlrpc.php
└── docker-compose.yml

4 directories, 18 files

I got myself a WordPress copy, ready to run, and in my host machine file system.

Trusting no one, I want to see if doing something on the host would modify the files on the container and vice-versa.

Two-way binding test

The first test will be to delete the "Hello Dolly" plugin from the host machine and see if the "Hello Dolly" plugin does disappear in the container:

rm ./_wordpress/wp-content/plugins/hello.php

A check-in the WordPress installation, at http://localhost:8080/wp-admin/plugins.php confirms my expectation:

Hello Dolly plugin removed

To test that changing something in the container would, in turn, change it on the guest I will try to install the Hello Dolly plugin from the WordPress plugin repository again; if all goes as intended, then it should re-appear on the host.

Hello Dolly plugin reinstalled

Repos/wp-docker » tree -L 1 ./_wordpress/wp-content/plugins
./_wordpress/wp-content/plugins
├── akismet
├── hello-dolly
└── index.php

2 directories, 1 file

And it does.

This confirms volume binding works as intended.

On macOS. Will it be the same on Linux? And Windows?
Spoiler: no, it requires some leg work and understanding of file modes that I will plunge into in my next post.