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
read-tree, without familiar tools like
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:
1 2 3 4 5 6 7 8 9
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).
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:
This pulls the image from the Docker repository (more about this later). Let’s start a container based on this image:
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:
1 2 3 4 5 6 7
So far, so good. Now let’s quit the bash prompt and see what happens:
What happened to our container? Run docker ps to get a list of running containers and it shows nothing.
docker ps -a (“a” for “all”) and you can see your container, along with an exit code indicating that it’s no longer running.
1 2 3
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:
And, inside our container, install our key dependencies:
After these packages have installed, we can exit from the container and use a new docker command –
1 2 3 4 5 6
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:
1 2 3 4 5
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:
1 2 3 4 5
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:
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
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:
1 2 3
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:
1 2 3 4
Now we can fire up a container based on our new image, extract Drupal, and configure MySQL and Apache:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
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:
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!
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.