Docker and docker-compose for WordPress testing - 03
June 12, 2020
Previously, in this series
In the first post in this series, I've covered the basic stuff: what Docker and docker-compose
are and how they could enable a portable environment for development, and more interesting to me, testing WordPress projects.
In the second post, I've detailed the behavior of the same docker-compose.yml
stack on macOS, Windows, and Linux machines.
I've concluded the post in a situation where files created by the WordPress container, on a Linux host, would not belong to my user, but another user.
In this third article, I will work to mitigate the "portability" issues arising from a stack that will behave differently depending on the host operating system (macOS, Windows or Linux) to move closer to the ideal of a portable testing environment for WordPress projects.
The docker-compose.yml
file in question, the one I'm starting this post from, is this one:
version: '3.1'
volumes:
db:
services:
wordpress:
image: wordpress
ports:
- 8080:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
volumes:
# 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
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql
The difference between host users and container users in Linux
The main difference between how Docker works on macOS and Windows versus how works on Linux is, without delving too deep into technical aspects I'm not expert enough to discuss, there is an additional layer (allow me to call it a "virtual machine") between the macOS or Windows host and the containers that is not present on Linux.
Containers are a Linux technology ported over to macOS and Windows. The ports solve several compatibility problems across different operating systems and mitigate, leveraging the use of "virtual machine" of sorts, some of the key differences between the OSes. Containers ignore the underlying implementation; in a way, Docker (for Windows, for macOS and Linux) acts as Java: create an image, build, publish it, share it, and run wherever Docker runs.
The differences become something to deal with and put some care into when sharing files between the host machine and the container. With some exceptions, most images are based on Linux OSes, where file permissions are serious business and not a small part of why Linux is the de-facto standard when it comes to servers.
When containers run on Linux, file ownership of shared files is maintained.
What this means is that:
If a file belongs to the user luca
on the host machine, then that file will belong to luca
in the container.
- Vice-versa, if a file belongs to
www-data
in the container, it will belong towww-data
on the host machine.
The second would explain why, in my previous post, I could not delete files created by the container from the host machine: my user on the host, luca
is not the owner of the files created by the www-data
user in the container.
When I tried to do that I would get the following output:
Repos/wp-docker » whoami
luca
Repos/wp-docker » ls -la _wordpress/wp-content/plugins/hello-dolly
total 16
drwxr-xr-x 2 www-data www-data 4096 Jun 6 16:58 .
drwxr-xr-x 4 www-data www-data 4096 Jun 6 16:58 ..
-rw-r--r-- 1 www-data www-data 2593 Jun 6 16:58 hello.php
-rw-r--r-- 1 www-data www-data 623 Jun 6 16:58 readme.txt
Repos/wp-docker » id -u
1002
Repos/wp-docker » id -u www-data
33
To delete the files from the host I am forced to use sudo
:
Repos/wp-docker » sudo rm -rf _wordpress/wp-content/plugins/hello-dolly
[sudo] password for luca: ***********
Using sudo
is a dangerous habit and a requirement I would like to avoid in this instance.
Fixing the file ownership issue in the WordPress container
The official WordPress image published on Dockerhub supports running Apache as a different user.
Looking at the docker-entrypoint.sh
file that will run when the container starts, I see I could set the APACHE_RUN_USER
and APACHE_RUN_GROUP
environment variables to have the Apache web-server process belong to a specific user.
What user? If on my host Linux machine my user is luca
, and I want my user to be able to modify and delete the files shared with the container without requiring the use of sudo
, then Apache must be started by luca
.
I will specify the user by its user ID (uid
) and group ID (gid
).
I've shown it before, but, as a refresher, I can fetch the current user ID and group ID from the terminal:
Repos/wp-docker » id -u
1002
Repos/wp-docker » id -g
1002
Usually, on Linux, those same values will be available on the UID
and GID
environment variables, but I prefer to rely on commands I run to make sure I'm getting the values I need.
I modify the docker-compose.yml
file to define the APACHE_RUN_USER
and APACHE_RUN_GROUP
environment variables:
version: '3.1'
volumes:
db:
services:
wordpress:
image: wordpress
ports:
- 8080:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
APACHE_RUN_USER: ${APACHE_RUN_USER}
APACHE_RUN_GROUP: ${APACHE_RUN_GROUP}
volumes:
# 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
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql
The
FOO: ${BAR}
notation means "at runtime set the value of theFOO
environment variable in the container to the current value of theBAR
environment variable on the host". There is no requirement for the two variables to have the same name; it's just convenient.
I try to spin up the stack again, specifying the two environment variables:
APACHE_RUN_USER=$(id -u) \
APACHE_RUN_GROUP=$(id -g) \
docker-compose up
Recreating wp-docker_wordpress_1 ... done
Attaching to wp-docker_wordpress_1
wordpress_1 | AH00543: apache2: bad user name 1002
wp-docker_wordpress_1 exited with code 1
Ok, this makes sense. The user luca
does not exist in the container; the Apache process cannot belong to a user that does not exist.
The docker-compose
specification allows specifying a user
, this is the equivalent of [using the -u|--user
option when using the docker run
command.
The documentation says:
When passing a numeric ID, the user does not have to exist in the container.
Which is what I need, I want to have a user somehow automagically created for me and I want to run Apache as that user.
I modify the docker-compose.yml
file again:
version: '3.1'
volumes:
db:
services:
wordpress:
image: wordpress
ports:
- 8080:80
user: ${WORDPRESS_RUN_USER}:${WORDPRESS_RUN_GROUP}
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
APACHE_RUN_USER: ${APACHE_RUN_USER}
APACHE_RUN_GROUP: ${APACHE_RUN_GROUP}
volumes:
# 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
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql
I try again setting the two new variables as well:
APACHE_RUN_USER=$(id -u) \
APACHE_RUN_GROUP=$(id -g) \
WORDPRESS_RUN_USER=$(id -u) \
WORDPRESS_RUN_GROUP=$(id -g) \
docker-compose up
wp-docker_wordpress_1 is up-to-date
Creating wp-docker_db_1 ... done
Attaching to wp-docker_wordpress_1, wp-docker_db_1
db_1 | 2020-06-09 11:44:26+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.30-1debian10 started.
wordpress_1 | sed: couldn't open temporary file ./sedEJZmHq: Permission denied
wordpress_1 | sed: couldn't open temporary file ./sedLW2kUW: Permission denied
wordpress_1 | sed: couldn't open temporary file ./sedbvCEiP: Permission denied
wordpress_1 | sed: couldn't open temporary file ./sedEf5PRI: Permission denied
wordpress_1 | sed: couldn't open temporary file ./sed8AOfWS: Permission denied
wordpress_1 | sed: couldn't open temporary file ./sedo8VJU5: Permission denied
wordpress_1 | sed: couldn't open temporary file ./sedpTR0UV: Permission denied
wordpress_1 | sed: couldn't open temporary file ./sed0jXAY3: Permission denied
wordpress_1 | sed: couldn't open temporary file ./sedxBnvZX: Permission denied
wordpress_1 | sed: couldn't open temporary file ./sedgVhJmU: Permission denied
Not what I had expected.
When I run the docker-compose up
command, I'm just starting a series of connected containers and running commands in each.
What does "running a command" means?
Containers are a "technology" to run commands in an isolated, self-contained environment.
As an example, the Nginx container will run the nginx -g daemon off
command, and the WordPress container I'm using in the current stack will run the apache2-foreground
command.
The commands, as is the case with the two I've just listed, might be commands to start a server, but any container image (from which containers are built) is essentially a way to prepare for, and run, a single command. The fact that a command might do something and return in a second or run for weeks does not change how containers work.
It's essential to understand this to appreciate why the user running the command is so important: if the user running the command, or any of the commands required to set up the environment for that command to run, is not authorized, then the container will exit.
By default, Docker containers will run using the root
user (id=0
).
As is the custom in Linux, the root
user is the one who can do everything.
The change, in the docker-compose.yml
file, that is causing the issue is the one where I specify a user
other than the root
one; that user is not authorized to write to /tmp
.
In this case, writing to the /tmp
directory is required while setting up the WordPress container.
I'm in a bit of a conundrum here: I want to run the final container command, apache2-foreground
, to start the Apache web-server as my user (luca
) but I cannot run the container as that user (what I just did), and I cannot specify, as APACHE_RUN_USER
, a user that does not exist in the container.
Modifying the WordPress container command
What I got from the previous section, is that each container is a way to run a command.
To run the command, in the WordPress container, I can modify its command
.
Note: specifying the
command
parameter in a service description in thedocker-compose.yml
file is the equivalent of specifying aCMD
directive in the imageDockerfile
.
On Linux, the user name, e.g. luca
is just a local (to the machine) name, and what really has value is the user ID and the ID of the user group.
To make a parallel with WordPress, the user ID is the post ID
, and the user name is the post_title
: a post will keep being uniquely identifiable as long as the post ID
stays the same, the title can change over time.
Knowing this, and keeping the objective of portability in mind, I call my user, the user that has the same user and group ID in the container that I have on my Linux machine (id=1002 gid=1002
), docker
. The user naming choice follows a common standard.
I modify the docker-compose.yml
file section related to the wordpress
container to:
- Remove the
user
parameter and let the container run usingroot
. - Create a
docker
user with the sameuid
andgid
as myluca
user on my Linux machine.
version: '3.1'
volumes:
db:
services:
wordpress:
image: wordpress
ports:
- "8080:80"
# The default entypoint is `/usr/local/bin/docker-entrypoint.sh`. Not overridden here.
# The default command is `apache2-foreground`, to start the Apache web-server.
# The command is overridden to create the `docker:docker` user and group mapped to the host machine
# user and group if they do not exist already.
# After creating the user and group, run the original command.
# The `$$` is to escape the `$` in YAML and avoid docker-compose trying to replace it.
command:
- /bin/sh
- -c
- |
test $$(getent group docker) || addgroup --gid ${APACHE_RUN_GROUP} docker
test $$(id -u docker) || adduser --uid ${APACHE_RUN_USER} --ingroup docker \
--home /home/docker --disabled-password --gecos '' docker
/usr/local/bin/docker-entrypoint.sh apache2-foreground
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
# Start the Apache server as the `docker` user.
# I've created the user in the `command` section, and this will work now.
APACHE_RUN_USER: "docker"
APACHE_RUN_GROUP: "docker"
volumes:
# 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
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql
When I run the docker-compose up
command again, everything will work.
APACHE_RUN_USER=$(id -u) \
APACHE_RUN_GROUP=$(id -g) \
docker-compose up
The WordPress installation files belong to my user:
Repos/wp-docker » ls -la _wordpress
total 224
drwxr-xr-x 5 luca luca 4096 Jun 10 15:39 .
drwxr-xr-x 3 luca luca 4096 Jun 10 15:39 ..
-rw-r--r-- 1 luca luca 234 Jun 10 15:39 .htaccess
-rw-r--r-- 1 luca luca 420 Dec 1 2017 index.php
-rw-r--r-- 1 luca luca 19935 Jan 1 2019 license.txt
-rw-r--r-- 1 luca luca 7368 Sep 2 2019 readme.html
-rw-r--r-- 1 luca luca 6939 Sep 3 2019 wp-activate.php
drwxr-xr-x 9 luca luca 4096 Dec 18 23:16 wp-admin
-rw-r--r-- 1 luca luca 369 Dec 1 2017 wp-blog-header.php
-rw-r--r-- 1 luca luca 2283 Jan 21 2019 wp-comments-post.php
-rw-r--r-- 1 luca luca 3183 Jun 10 15:39 wp-config.php
-rw-r--r-- 1 luca luca 2808 Jun 10 15:39 wp-config-sample.php
drwxr-xr-x 4 luca luca 4096 Jun 10 15:39 wp-content
-rw-r--r-- 1 luca luca 3955 Oct 11 2019 wp-cron.php
drwxr-xr-x 20 luca luca 12288 Dec 18 23:16 wp-includes
-rw-r--r-- 1 luca luca 2504 Sep 3 2019 wp-links-opml.php
-rw-r--r-- 1 luca luca 3326 Sep 3 2019 wp-load.php
-rw-r--r-- 1 luca luca 47597 Dec 9 2019 wp-login.php
-rw-r--r-- 1 luca luca 8483 Sep 3 2019 wp-mail.php
-rw-r--r-- 1 luca luca 19120 Oct 15 2019 wp-settings.php
-rw-r--r-- 1 luca luca 31112 Sep 3 2019 wp-signup.php
-rw-r--r-- 1 luca luca 4764 Dec 1 2017 wp-trackback.php
-rw-r--r-- 1 luca luca 3150 Jul 1 2019 xmlrpc.php
I can now delete the files from my host machine without using sudo
:
Repos/wp-docker » rm -rf _wordpress/wp-content/plugins/hello.php
Repos/wp-docker » ls -la _wordpress/wp-content/plugins
total 16
drwxr-xr-x 3 luca luca 4096 Jun 10 15:56 .
drwxr-xr-x 4 luca luca 4096 Jun 10 15:39 ..
drwxr-xr-x 4 luca luca 4096 Dec 18 23:16 akismet
-rw-r--r-- 1 luca luca 28 Jun 5 2014 index.php
And, after I've installed WordPress using its UI at http://localhost:8080
, if I install a plugin from the WordPress installation, tat plugin files too will be accessible to my user.
The docker-compose.yml
file, in its wordpress
service section especially, could be reduced by building a dedicated image customized to my needs. Currently, I prefer the compact form of the single docker-compose.yml
file and will leave that for later, if required.
Thinning the command to start the stack
It's a bit cumbersome to have to start the stack using this command:
APACHE_RUN_USER=$(id -u) \
APACHE_RUN_GROUP=$(id -g) \
docker-compose up
It's not impossible to remember, but it's error-prone, and, as the stack evolves, it will become more and more complex.
To get back the ease-of-use I've created a PHP script that will take care of running the command correctly every time with awareness of the current operating system:
#! /usr/bin/env php
<?php
// Default to `up` if no arguments are provided.
// $argv[0] is the script name itself, drop it.
$unescapedArgs = isset($argv[1]) ? (array)array_slice($argv,1) : ['up'];
// Escape the command arguments before using them.
$escapedArgs = array_map('escapeshellarg', $unescapedArgs);
// Get the current OS; `dar` is macOS, `lin` is Linux, `win` is Windows
// Other OSes (*nix, BSD...) require, probably, the same setup steps as for Linux.
$os = substr(strtolower(PHP_OS), 0, 3);
$binary = 'docker-compose';
switch ($os){
case 'win':
$binary = 'docker-compose.exe';
break;
case 'dar':
// There is nothing to do here.
break;
default:
// If we cannot get the current user and group ID use `0` (`root` in the container).
putenv('DOCKER_RUN_USER=' . (int)getmyuid());
putenv('DOCKER_RUN_GROUP=' . (int)getmygid());
break;
}
// Build the command line.
$commandLine = sprintf('%s %s', $binary, implode(' ', $escapedArgs));
// Debug the command line.
echo "\nCommand: {$commandLine}\n\n";
// Let's not apply the time limit to the processes launched by the script.
set_time_limit(0);
// Execute the command.
passthru($commandLine, $status);
// Print a blank line after the command output to clear the terminal a bit.
echo "\n";
// Exit the same status as the command.
exit($status);
I've called the script stack
, so I can now run the equivalent of the previous command by running:
./stack up
I've modified the stack to work on Linux, will it be the same on Windows And macOS?
In my next post, I will test the current solution out on both to move a step closer to a portable WordPress testing environment and iterate on the CLI wrapper script.