Drupal on Docker

May 12, 2013

At the time of writing, the current version of Docker is 0.3.2 - future versions may change some of the details in this post, and I’ll try to keep it up-to-date when this happens

Docker is a new open source tool from dotCloud, which simplifies and improves the process of creating and managing Linux containers (LXC). If you’re unfamiliar with Linux containers, the easiest way to think of them is like extremely lightweight VMs - they allow Linux operating systems to host other copies of Linux, safely sharing access to resources without the overheard of something like Xen or VirtualBox. This is useful because there are many cases where full-blown VMs are not necessary, and performance and flexibility can be gained by using containers instead. (On the negative side, containers are Linux-only; both the host and guest operating systems must be Linux-based).

This would be great if containers were easy to use. Unfortunately, they’re not; at least, not as easy to use as they could be. Using containers is a bit like trying to use git using only commands like update-index and read-tree, without familiar tools like add, commit and merge. Docker provides that layer of “porcelain” for LXC, turning containers into something that are much easier to think about, manipulate and distribute.

To get started, we need an environment to run Docker in. If you already run Ubuntu as your primary OS then this process is probably unnecessary and you can skip straight to installing Docker. For everyone else (including me), the easiest solution is to install a Ubuntu VM (LXC requires modern Linux kernels for host systems, and Ubuntu has the best tooling at present, though Arch is also supported). To get this running, install Vagrant (documentation here) and create a Vagrantfile as follows:

Vagrant::Config.run do |config|
  config.vm.box = "raring"
  config.vm.box_url = "http://cloud-images.ubuntu.com/raring/current/raring-server-cloudimg-vagrant-amd64-disk1.box"
  config.vm.forward_port 80, 8880
  config.vm.share_folder("v-root", "/vagrant", ".")
  Vagrant::Config.run do |config|
    config.vm.customize ["modifyvm", :id, "--memory", 512]

This should get you a VM running Ubuntu 13.04. Run vagrant up and your new Ubuntu environment should download and boot and you can use vagrant ssh to log in. You can then follow the Docker installation instructions. Once installed, you can run docker commands (enter docker at the command line to get a list of available commands).

Docker Basics

We’re now ready to start doing interesting stuff.

Before we can go any further, we need a base image for our guest operating system. This has to be a Linux distro (non-Linux operating systems are not supported) but it doesn’t have to be the same version as the host, or even the same distro. In this example, let’s pull a CentOS guest image:

$ docker pull centos

This pulls the image from the Docker repository (more about this later). Let’s start a container based on this image:

$ docker run -i -t centos /bin/bash

You should now be logged in to your container as root. From the shell prompt, we can do any of the things we’d normally do - install applications Remember, however, that the CentOS base image we downloaded is very minimal, containing only the bare essentials. We’ll probably want to install some additional packages:

$ yum install ruby-irb
# ...yum output...
$ irb
irb(main):001:0> puts "Hello, docker"
Hello, docker
=> nil

So far, so good. Now let’s quit the bash prompt and see what happens:

$ exit

What happened to our container? Run docker ps to get a list of running containers and it shows nothing.

$ docker ps

Run docker ps -a (“a” for “all”) and you can see your container, along with an exit code indicating that it’s no longer running.

$ docker ps -a
ID             IMAGE        COMMAND      CREATED          STATUS      COMMENT     PORTS
969373734016   centos:6.4   /bin/bash    2 minutes ago    Exit 0

Unlike VMs, containers don’t need to boot up or shut down the whole OS. Unless you’re running a process in it, your container isn’t taking up any resources apart from some disk space. Once our bash process has finished, your container is closed.

What about the changes made to the filesystem - the Ruby package we installed, for instance? That’s still there, and will remain until the container is deleted (using docker rm). We can get back to our container by “restarting” it - this takes whatever command you ran originally (in our case, /bin/bash) and runs it again. docker restart will start your container in the background, so you need to run docker attach to start interacting with it. Voila, our bash prompt returns!

So far, we’ve seen how to download a base image, start a container, make some changes in it, exit, and restart. What about creating images? If we always have to start from a minimal base image then we’re going to waste a lot of time installing things. Let’s say that you want to use docker for testing LAMP-stack applications - it would be really useful to have a base image that included Apache, PHP and MySQL. Fortunately, that’s very easy to do. Let’s start a new container:

$ docker run -i -t centos /bin/bash

And, inside our container, install our key dependencies:

$ yum install httpd php php-common php-cli php-pdo php-mysql php-xml php-mbstring mysql mysql-server

After these packages have installed, we can exit from the container and use a new docker command - commit:

$ exit
$ docker ps -a
ID             IMAGE        COMMAND      CREATED          STATUS      COMMENT     PORTS
1ae6304e2514   centos:6.4   /bin/bash    5 minutes ago    Exit 0
$ docker commit 1ae6304e2514 LAMP

What this does is save our container as a new image, which can be used as the starting point for new containers in future. The number which is printed after docker commit is the ID of our new image. We can see it in the list of images:

$ docker images
REPOSITORY          TAG                 ID                  CREATED
centos              6.4                 539c0211cd76        5 weeks ago
centos              latest              539c0211cd76        5 weeks ago
LAMP                latest              bd2f18527e54        9 seconds ago

So, we’ve got a new base image, called LAMP, which comes with PHP, MySQL and Apache pre-installed. Let’s start a container with it:

$ docker run -i -t -p :80 LAMP /bin/bash
$ php -v
PHP 5.3.3 (cli) (built: Feb 22 2013 02:51:11)
Copyright (c) 1997-2010 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2010 Zend Technologies

Great, PHP is installed and working. Did you see the -p :80 parameter in the docker run command? That tells docker to forward port 80 from the container to the host. This means that if we run Apache on port 80 inside the container, we’ll be able to connect to it on port 80 on the host system. If you’re running the host OS inside Vagrant then the Vagrantfile earlier in this post will forward that back to port 8880 on your main system.

Within the container, start Apache:

/sbin/service httpd start

Now, hit the server (http://localhost:80 if you’re running Docker natively, http://localhost:8880 if you’re running it inside Vagrant). You should see something like this:

Apache test page

OK, this is great, but nobody wants to see the Apache test page. We need a way of deploying some code to our container. Given that we have shell access to our container, we could just download some code using wget or scp or any number of other tools. Right now, the approved way of downloading code to a Docker instance is to use docker insert (I say “right now” because insert is a recently-added command, and it’s possible that its behaviour may change). To download Drupal core:

$ docker insert LAMP http://ftp.drupal.org/files/projects/drupal-7.22.tar.gz /root/drupal.tar.gz
Downloading 3183014/3183014 (100%)

This downloads the file into our LAMP image. The long hash code which is printed to the screen is the ID of the new image which is created as a result (you can’t modify an image directly, except by creating a new image which inherits from it - think of this as being like a new git commit).

Having to use a 64-character ID every time we want to use our image would get annoying, so let’s ‘tag’ it:

$ docker tag 26d9b3e0438f631b2f030eedffb6b14216261c9e4ecab035e9123ebf4e3460e7 LAMP drupal
$ docker images
LAMP                latest              bd2f18527e54        22 hours ago
LAMP                drupal              26d9b3e0438f        9 seconds ago

Now we can fire up a container based on our new image, extract Drupal, and configure MySQL and Apache:

$ docker run -i -t -p :80 LAMP:drupal /bin/bash
# Extract Drupal to /var/www/drupal
$ tar zxf /root/drupal.tar.gz --strip=1 -C /var/www/html
# Slight hack needed to get MySQL to start
$ echo "NETWORKING=yes" > /etc/sysconfig/network
$ /sbin/service mysqld start
Initializing MySQL database:  Installing MySQL system tables...
# MySQL will now print some messages
Starting mysqld:                                           [  OK  ]
# Let's create a MySQL DB
$ mysql -uroot
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.1.69 Source distribution

Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> create database drupal;
Query OK, 1 row affected (0.00 sec)
mysql> grant all privileges on drupal.* to 'drupal'@'localhost' identified by 'drupal';
Query OK, 0 rows affected (0.00 sec)
mysql> flush privileges;
mysql> exit
# Create the Drupal settings file and point to the DB we just created
$ cp /var/www/html/sites/default/default.settings.php /var/www/html/sites/default/settings.php
# Edit the database settings so that the DB name is 'drupal', username is 'drupal' and password is 'drupal'
$ vi /var/www/html/sites/default/settings.php
# Set up a files directory and some basic file permissions
$ mkdir /var/www/html/sites/default/files
$ chown apache:apache /var/www/html/sites/default/files
# Fire up Apache
$ /sbin/service httpd start
Starting httpd:                                            [  OK  ]

Remember when we hit the Apache test page earlier? Go back to the URL we used then and add /install.php on the end, so you get http://localhost/install.php if you’re running Docker locally, or http://localhost:8880/install.php if you’re running Docker inside Vagrant. If everything worked, you get this:

Drupal installation screen

From here, you can complete your Drupal installation. Once you’ve finished, you can commit your new image, and then you have a base image with Drupal pre-installed. The end!

Some questions

That took a long time. Can we automate some of it?

Yes! In a real scenario, logging in and typing in shell commands is a painful way to create an image. Docker supports “build” files which are simple scripts that can be used to perform steps such as running commands, importing files and exposing forwarded ports. So long as you don’t kick off any long-running processes, a build file typically takes only a few seconds to run and will create your image for you.

Can I share my image?

Yes! The Docker repository is a place where you can push images you’ve created, and download images created by others. The Docker index provides a web front end to search for images. The image created in the above example is uploaded here.

What can I actually use Docker for?

This is a big question. Right now, Docker is new and there will be use cases that nobody has thought of, or had time to investigate. But some of the obvious ideas are:

  • Deployment If you can build an image that works on your desktop, you can run it on a server too. Because the image includes everything - a full copy of the OS and all dependencies (PHP, MySQL and Apache in our example, but it could be anything), then you don’t need to worry about which versions are running in which environments. You just need a server that can run Docker.
  • Testing If we can script the construction of containers, and we can script Docker itself (using shell scripts, or Rakefiles, or whatever) then we could build an automated testing process using Docker to create our test environments. Say you want to know if your application works in all versions of CentOS, or works across CentOS, Ubuntu and Arch; you could have Docker build files for each distro version, and run these every time you want to create images with the latest version of your application for testing.
  • Isolation Because processes inside the container are isolated from the host, it’s a great way of running code safely. If your application requires several processes to run, you could put each one inside a different container. The containers could even be running different distributions, meaning that your battle-tested enterprise service can run in a RHEL 6 container and your bleeding-edge NodeJS application can run in a Ubuntu 13.04 container.

Ulimately, Docker could change how we see “applications”, at least ones that are deployed on servers in the cloud. Instead of your application being a thin layer of code that sits on top of multiple layers of services over which you have little control, you could package everything in a known working combination, and ship that instead.