Switching from Vagrant to Docker for WordPress Development

These past few weeks I’ve been experimenting with Docker as an alternative to Vagrant (and VVV) for WordPress development.

I had looked into Docker in the past, but given how easy Vagrant and VVV were, I was in no hurry to switch. VVV still is awesome (there is an excellent getting started guide here), but since settling on Vagrant, the tools and ecosystem around Docker have significantly improved.

Mainly to satisfy my own curiosity, but also hoping to use less system resources (since I currently code on a Macbook Air) I finally gave Docker a fair chance. I wanted to share some of the things I’ve learned over the past week whilst switching.

Docker and Vagrant differences

Vagrant uses boxes (virtual machines) to power your webserver. These boxes run operating systems of their own in isolation from the host machine.

Docker on the other hand shares some resources with the host machine, and creates virtual environments called containers.

There are pros and cons to both systems, but I don’t want to delve in to that. Both work well. Instead I just want to show how things work in Docker vs Vagrant.

VagrantDocker
Software requirementsVagrant, VM Software such as VirtualBoxDocker engine, Docker desktop (optional)
EnvironmentsVagrant BoxesDocker Images
Provisioning a machineUse VVV or define a custom VagrantfileRun a predefined image, create a docker-compose.yml file, or use WP Local Docker.
Starting a machinevagrant up --provisiondocker run <image> or docker-compose start
Stopping a machinevagrant suspenddocker-compose down
Destroy a machinevagrant destroydocker-compose down --volumes
Accessing the command line of your environmentvagrant sshdocker exec -it <container> /bin/bash

Both are fairly similar in usage. Docker Desktop however does provide a nice GUI to see running containers which is nice.

Setting up Docker

Your first task is to install Docker. If you’re on a Mac, it’s as simple as installing Docker Desktop. As well as access to Docker on the command line, this also gives you a GUI to view, stop, run, or destroy your Docker containers.

Docker Desktop showing some running containers

Creating a WordPress container

To keep it simple as possible, open up Terminal and run the official WordPress image.

docker run -p 8088:80 wordpress

While this is running on the command line you can access this WordPress install by pointing your browser at https://localhost:8088. Stop it using control+c.

You can also run this image in “detached” mode by appending the -d flag – to stop it you can use Docker desktop, or find its container ID and run the stop command.

Magic ๐Ÿง™โ€โ™‚๏ธ

Creating instances of WordPress with docker-compose

You can create a file named docker-compose.yml to define what containers should be setup for your project. This is documented here, but to give a basic WordPress example, take the following:

version: '3.3'

services:

  db:
    image: mariadb:10.4
    restart: always
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress
      MYSQL_RANDOM_ROOT_PASSWORD: 'yes'
    volumes:
      - db:/var/lib/mysql

  wordpress:
    container_name: wordpress
    depends_on:
      - db
    image: wordpress:latest
    ports:
      - 8082:80
    restart: on-failure
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_TABLE_PREFIX: wp_
      WORDPRESS_DEBUG: 'true'
    volumes:
      - "./wp-content:/var/www/html/wp-content"
      - wordpress:/var/www/html

volumes:
  db:
  wordpress:

This file does a few things:

  • It tells Docker I want a database container running mariadb with the user login details I provide.
  • It tells Docker I want a WordPress container, using the official WordPress image, which depends on the database container.
  • It gives WordPress the database login details.
  • It creates a shared volume on my host machine in the same path as my docker-compose.yml file where I want to place wp-content files, such as themes and plugins.

In terminal, if I navigate to the directory holding this file, I can run docker-compose start to start these containers, and then access the WordPress site at http://localhost:8082. These containers will also be visible in Docker Desktop.

Using WP Local Docker to simplify the process

VVV simplified the use of Vagrant, and WP Local Docker, created by 10up, does the same for Docker.

It handles the creation of containers, using versions of PHP you choose, and handles the setup of things such as Networking and your hosts file (which gives you nice URLs rather than localhost:xxxx).

Installation instructions can be found here. Once installed, you can spin up a local WordPress environment using the command 10updocker create and following the prompts:

This is the method I settled on using.

Sharing data with bind mounts

Containers by design have no access to the host machine. Therefore, if you are developing within a container (for example building WP plugins) you need a way to edit files.

Bind mounts, documented here, do just that, and allow you to share files between your host machine and a Docker container.

In my case, to keep my files organised I like to keep my local plugin git repositories together in a single directory. To allow my WordPress container to see those files I set up a bind mount.

If you edit the docker-compose.yml file generated by WP Local Docker (this defaults to the ~/wp-local-docker-sites/<environment>/ directory) you’ll see two sections which look something like this:

volumes:
      - './wordpress:/var/www/html:cached'

These are the volumes and bind mounts. We can add to these to map more directories so that the container can access them:

volumes:
      - './wordpress:/var/www/html:cached'
      - '~/Sites/plugins:/var/www/html/wp-content/plugins'
      - '~/Sites/themes:/var/www/html/wp-content/themes'

After restarting the container, the container will be able to use and see any files in the local ~/Sites/plugins and ~/Sites/themes directories.

You can of course be even more specific and set bind-mounts for specific plugins. For example, if I only wanted a bind mount for the WooCommerce plugin I could use:

volumes:
      - '~/Sites/plugins/woocommerce:/var/www/html/wp-content/plugins/woocommerce'

When using VVV, where you can see most of the files on the webserver locally by default, I set a similar system up using symlinks. Docker can be a little more tricky to grasp at first, but once you understand how containers work I think I prefer it.

Running commands in a Docker container

With VVV, if you needed to access the filesystem of a box you could use vagrant ssh to tunnel in and gain access to the command prompt.

With Docker, you can do something similar with docker exec which lets you execute commands on your container.

Want a bash prompt? Use docker exec -it <container_name> /bin/bash. You can do the usual things such as exploring the filesystem and can exit when you’re done.

Running PHPUnit

This was the most challenging aspect for me when switching from VVV.

VVV comes preconfigured to run WordPress unit tests using the WP test suite. In the case of the plugins I work on daily, running tests involved SSHing in and running phpunit as follows:

cd /path/to/plugin/
.vendor/bin/phpunit

The WordPress Docker images only contain WordPress, so in order to run tests I needed to create a custom container with the WP Test Suite setup. I needed:

  • a docker-compose.yml file like the example earlier with just a WordPress container and a container for the database.
  • Rather than specifying the WordPress image, I created a custom Dockerfile. Dockerfiles let you execute commands when a container is built, and even let you utilise multiple containers during this process.
  • A new NPM task to spawn the container and run PHPUnit automatically (so other developers can use this easily).

To use a DockerFile, docker-compose.yml gets a section like this in the place of image:

    build:
      context: ./tests/bin

This tells docker-compose where to look for my Dockerfile. The Dockerfile itself can access anything in it’s directory, and in my case looked like this:

FROM wordpress:latest

RUN apt-get update &amp;&amp; apt-get -y install subversion unzip wget git-all

CMD echo "Installing tests..."

ENV WP_TESTS_DIR=/tmp/wordpress-tests-lib
ENV WP_CORE_DIR=/usr/src/wordpress

COPY install-wp-tests.sh /usr/local/bin/dockerInit
RUN chmod +x /usr/local/bin/dockerInit
RUN dockerInit wordpress wordpress wordpress db latest true

# This is the entrypoint script from the WordPress Docker package.
CMD ["docker-entrypoint.sh"]

# Keep container active
CMD ["apache2-foreground"]

The basic gist of this is:

  • Using the WordPress image as a base, install some dependencies (SVN, GIT, and the unzip/gzip commands used elsewhere)
  • Setup environment variables used by install-wp-tests.sh
  • Copy install-wp-tests.sh to the container (this bash script installs the test suite) and make it executable.
  • Run the docker-entrypoint script (copied from the WordPress image) which sets up WordPress.

Now when this container is running, I can trigger PHPUnit to run with the following exec command:

docker exec -it --workdir /path/to/my/plugin <container_name> php ./vendor/bin/phpunit

You can see these changes in context in this pull request on GitHub which I raised to add a way to run unit tests on the WooCommerce Blocks plugin with Docker.

And that’s about it! I’ve only been on this setup a few days so it’s still early for a final judgement, but I’ve not hit any issues using Docker so far and it’s been fun to try something new. I hope this post is useful if you decide to try it!

Posted by Mike Jolley

I help build things at Automattic.

4 Replies to “Switching from Vagrant to Docker for WordPress Development”

  1. Noting in the title “Docker for WordPress Development”, are you actually attaching a debugger to the docker wordpress? or is it really unit testing as described in the post..

    Reply

    1. Yes you can! WP Local Docker has some brief instructions here.

      1. You have to edit the docker-compose.yml file and set ENABLE_XDEBUG: 'false' to true.
      2. Stop then start the container.
      3. I use VS Code which has a PHP DEBUG extension: https://marketplace.visualstudio.com/items?itemName=felixfbecker.php-debug
      4. In its launch.json config you give it a path config matching your container. For me, I ended up with this section:

      {
      	"name": "Listen for XDebug",
      	"type": "php",
      	"request": "launch",
      	"port": 9000,
      	"pathMappings": {
      		"/var/www/html/wp-content/plugins": "/Users/mikejolley/Sites/plugins",
      		"/var/www/html": "/Users/mikejolley/Sites/local-wordpress-test/wordpress"
      	}
      }

      Reply

  2. Is it possible to setup a docker compose file that you can run separately so that you can include inside your repo and if others pull the repo can get up and running without having to setup each site?

    Reply

    1. Sounds similar to the phpunit example I gave? For simple test sites with a specific test case (e.g. testing a plugin) that can work.

      We have one for e2e tests too where the Docker compose file provisions the environment, and a custom Dockerfile/entrypoint installs required plugins and themes using the WP CLI.

      Reply

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.