Web development with Docker and Drupal on OS X

Docker has just been released for Mac OS X and it takes a different approach of using containers.  It claims to be faster and quicker to set up then alternatives such as virtual machines.  I will look at using Docker to set up a basic local development environment for a Drupal 8 website on Ubuntu 16.04.  I will be using PHP 7.0 and MariaDB.  This is done for an actual website and some problems are encountered which will demonstrate how to debug and the tools that can be used to solve issues.

The best practice for setting up containers using Docker is to set up a different container for each function, such as one container for the database server and another for the webserver.  This is so that containers can be reused but that is not a concern here.  I will be taking a different approach of matching the container to the live server.  For simple websites with the database server and webserver on the same machine this corresponds to a single container.  I also found it confusing to start using Docker and immediately be thrown into using multiple containers; a single container is a better starting point for learning Docker.

Docker uses a Dockerfile to automate the build of an image.  The syntax is fairly simple and easy for someone with a Unix background to pick up.

FROM    ubuntu:16.04
MAINTAINER      John Smith <john@example.com>

# Build argument for debian frontend
ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get upgrade -y

RUN apt-get install -y \
  apache2 \
  apache2-utils \
  curl \
  git \
  libapache2-mod-php7.0 \
  mariadb-client \
  mariadb-server \
  php7.0-cli \
  php7.0-common \
  php7.0-curl \
  php7.0-gd \
  php7.0-mysql \
  php-xml \
  vim \
RUN apt-get clean


COPY conf/$WEBROOT.conf /etc/apache2/sites-available/
RUN a2enmod rewrite; a2ensite $WEBROOT.conf

# Setup PHP.
RUN sed -i 's/display_errors = Off/display_errors = On/' /etc/php/7.0/apache2/php.ini

#Install Drush 8 (master)
RUN php -r "readfile('http://files.drush.org/drush.phar');" > /usr/local/bin/drush; chmod +x /usr/local/bin/drush; drush init -y

# Install Drupal Console.
RUN curl http://drupalconsole.com/installer -L -o drupal.phar
RUN mv drupal.phar /usr/local/bin/drupal && chmod +x /usr/local/bin/drupal
RUN drupal init

EXPOSE 80 3306

CMD /etc/init.d/mysql start; /usr/sbin/apache2ctl -D FOREGROUND

As you can see the amount of code required is not a lot and much of it is easy to understand but I will go through it in detail. The FROM statement is required and specifies the base distribution to be used.  In this case we are using Ubuntu 16.04.  The MAINTAINER shows who wrote the Dockerfile but is not so important unless you are distributing your image.  The first ARG suppresses errors although it should detect that there is not terminal attached if this is omitted.  ARG is used instead of ENV because the latter persists when the container is running.  The next set of commands uses RUN to install packages for Ubuntu and should make sense to anyone familiar with Ubuntu.  I am installing MariaDB and PHP 7.0 for this example.  It should be fairly easy to change to other software since the statements are the same as used in setting up an Ubuntu server.

The next line we set an ARG for the webroot.  The default value is "drupal" but I will show later how to override this when building an image.  The following two lines copy over a configuration file for Apache and enables mod_rewrite.  Since this is a development environment we turn on display of errors for PHP.

Two of the basic utilities for development of Drupal 8 are Drush and Drupal Console.  The next set of lines just install these utilities.

The last few lines are more important.  The EXPOSE command lists that ports that should be accessible from the host computer.  The ports for the webserver and database are listed in this comand.  CMD is the commands that will be run when the container is run.  In this case I start up the database server and then the webserver.  Usually these run in the background but Docker containers will only run as long as they are running something.  So to keep the container from shutting down I place the webserver in the foreground.

Now that there is a Dockerfile it remains to show how to use it. The command for building the image passes in the name of the web root directory and tag the image. The dot at the end instructs to use the Dockerfile in the current directory.

docker build --build-arg WEBROOT=gaiaes -t docker_gaiaes .

The build takes 2.5 minutes on my computer with caching turned off. It does not seem like download times are included in that time. Subsequent builds will be even quicker since intermediate steps are cached.

Now that we have built an image we need to set up the files. It is better to set these up before starting the container.

tar xzvf gaiaes.tar.gz
cp conf/settings.local.php gaiaes/sites/default/

It would be just as easy to use Git to retrieve the files. We copy over a local configuration file for connecting to the database. The next step is to run a container using the image that we just built.

docker run --name gaiaes -v $(pwd)/gaiaes:/var/www/gaiaes:rw -p 8080:80 -d docker_gaiaes

The name argument assigns a name to the container to make it easier to reference it with other commands. The "-v" loads the files we just set up at the specified directory in the container. The "-p" maps the port from the host computer to the container. Any port for the container specified here should be in an EXPOSE command in the Dockerfile. The "-d" runs this in the background and then the image specified in the build command appears. To see what containers are running use

docker ps

The output from this command should be something like

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                            NAMES
c79af5e565fc        docker_gaiaes       "/bin/sh -c '/etc/ini"   3 minutes ago       Up 3 minutes        3306/tcp,>80/tcp   gaiaes

Add the argument "-a" to this command will list all containers including containers that are not running. The container ID can be used in commands instead of the name of the container. To stop a container use

docker kill gaiaes

To remove a container use

docker rm gaiaes

Now that we have built an image and created a running container, we need to finish setting up a website for development. First we fix permissions for some of the files and then load the database. The database snapshot is stored in the db directory.

docker exec -i gaiaes chmod -R u+w /var/www/gaiaes
docker exec -i gaiaes chgrp -R www-data /var/www/gaiaes/sites/default/files
docker exec -i gaiaes chmod -R g+w /var/www/gaiaes/sites/default/files
cd db
docker exec -i gaiaes mysql -u root -e "CREATE DATABASE gaiaes"
docker exec -i gaiaes mysql -u root -e "GRANT ALL PRIVILEGES ON gaiaes.* TO gaiaes@localhost IDENTIFIED BY 'xxxxxxxxx'"
gzip -c $(ls *.sql.gz) | docker exec -i gaiaes mysql -u root gaiaes

The docker ps command gave us the address of the website which in this example is Going to this address the browser shows

The provided host name is not valid for this server.

This is from the new trusted hosts settings so we go to gaiaes/sites/default and edit the settings.local.php to include

$settings['trusted_host_patterns'] = array(

Once this is fixed we reload the browser and get a white screen. So this is where we get to figure out tools for fixing problems. The Apache log files in the the container might be useful. So we need a shell in the container. This is done like this

docker exec -i -t gaiaes /bin/bash

The "-t" attaches a terminal. Looking at /var/log/apache2/error.log we see

Error: Call to undefined function twig_template_get_attributes() in /var/www/gaiaes/sites/default/files/php/twig/097b726f_views-view.html.twig_2752d856ded5806e196360155bb7918f5aece651c30f57fc2f0e6f955d472352/8fcdb99213826e5ed0d71311301490726a35c81b80abd7de9f3364b3f75a623c.php on line 50

To fix this we go to gaiaes/sites/default/files/php/twig and delete all the files other than the .htaccess file. Once this is done then we can reload the browser and the website comes up. To exit the shell we can use control-p control-q.