(note: this post was first published on the blog of the Dutch Web Alliance)
Docker is currently one of the hottest technologies around, because it solves a very specific problem: the ability to easily package and deploy a (self contained) application, without the overhead of traditional virtualization solutions.
In this post you’ll learn how to build, run and host Docker containers, integrate with other containers, and see how Vagrant interacts with Docker.
Background
The Linux kernel contains a number of containment features that enable resource (CPU, network, memory, etc.) & process (user id’s, file systems, process trees) isolation on the same host, without the need for a virtual machine: cgroups and namespaces.
This operating system-level virtualization imposes far less overhead. User space instances (also called containers) run within the host OS, so a hypervisor and guest OS are not needed. This results in lightweight images and incredibly fast start/boot times: think seconds, not minutes. Containers are like vm’s, without the associated weight.
Docker is a toolkit built around those containment features. Before version 0.9, Docker relied on LXC (LinuX Containers): a (userspace) interface to the kernel containment features. As of version 0.9, Docker has its own interface layer called libcontainer.
Installing Docker
Installing Docker is easy if you are using Ubuntu. To install the official package (may not be the absolute latest version):
$ sudo apt-get update $ sudo apt-get install docker.io $ sudo ln -sf /usr/bin/docker.io /usr/local/bin/docker $ sudo sed -i '$acomplete -F _docker docker' /etc/bash_completion.d/docker.io $ source /etc/bash_completion.d/docker.io
Should you really want the latest version:
$ curl -s https://get.docker.io/ubuntu | sudo sh
Note: because Docker relies on Linux-specific kernel features, it is not natively supported on OSX or Windows.
There are some workarounds: use Vagrant (see below), or refer to the Docker installation instructions ( https://docs.docker.com/installation/windows/ and https://docs.docker.com/installation/mac/) for details.
Creating and starting a container
Let’s start with a small PHP app, which we will package and deploy as a Docker container. The app will expose a simple socket server on a port (1337), and output a string to connections on that port. To build the app we’ll be using React, a library that adds event-driven, non-blocking IO to PHP.
First, install composer (http://getcomposer.org), then run the following command:
$ composer require react/react=0.4.x
Then save the following code as “app.php”:
<?php require 'vendor/autoload.php'; $loop = React\EventLoop\Factory::create(); $socket = new React\Socket\Server($loop); $http = new React\Http\Server($socket, $loop); $app = function ($request, $response) { $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello World\n"); }; $http->on('request', $app); echo "Server running at http://0.0.0.0:1337\n"; $socket->listen(1337, "0.0.0.0"); $loop->run();
If we then run the following command:
$ php app.php
we should see something like this:
Server running at http://0.0.0.0:1337
Open your browser, point it to http://localhost:1337/, and you should see something like:
Now, to get our app to run inside a Docker container, we first need to build an image. The instructions that define how to build the image are saved in a Dockerfile, in the form of commands:
FROM jolicode/php55 ADD ./ /var/apps/reacttest CMD cd /var/apps/reacttest && php app.php EXPOSE 1337
In this Dockerfile we’ve used the following commands:
- FROM: an existing image that our image should be based on
- ADD: a tar file or existing directory pointing to our app
- CMD: command to run when starting the container
- EXPOSE: ports that we want accessible outside of the container
The full list of commands is available .
Now that our Dockerfile is finished, let’s build the image. Run the following command:
docker build -t "our_app_image" .
This builds a new image using the Dockerfile in the current directory, and will tag
the resulting image with the string “our_app_image”. The output of the command should look like this:
Sending build context to Docker daemon 1.633 MB Sending build context to Docker daemon Step 0 : FROM jolicode/php55 ---> 13f8ff6325db Step 1 : ADD ./ /var/apps/reacttest ---> 09b79e178ff3 Removing intermediate container 20296bc15647 Step 2 : CMD cd /var/apps/reacttest && php app.php ---> Running in 8d907cb22524 ---> 66feac5b7e2c Removing intermediate container 8d907cb22524 Step 3 : EXPOSE 1337 ---> Running in 42b14e4d20d3 ---> ad72a42f978a Removing intermediate container 42b14e4d20d3 Successfully built ad72a42f978a
The next step is creating and running a new container that uses our freshly baked image.
docker run -d --name "our_app_container" -p 1337:1337 our_app_image && docker ps
The container will be named “our_app_container”, and will run in the background (the ‘-d’ option). Docker will also proxy port 1337 on the host to port 1337 in the container. After starting the container, ‘docker ps’ shows the list of running containers and their attributes, something like:
5c35b78eede36eb34ca7498b202979200df56dce6343cc5f0d8242f370bba64f CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 5c35b78eede3 our_app_image:latest /bin/sh -c 'cd /var/ Less than a second ago Up Less than a second 0.0.0.0:1337->1337/tcp our_app_container
To verify that the app is running and available, we can use ‘docker logs app’ to retrieve the stdin/stderr streams of the running container.
Dependencies: linking containers
Let’s extend our app with an external dependency. The requirement: increase a counter with every request to port 1337. The counter will live inside a Redis instance.
First, add predis-async to the list of composer dependencies:
$ composer require predis/predis-async=dev-master
Then, insert the following lines in app.php:
$client = new Predis\Async\Client('tcp://redis:6379', $loop); $app = function ($request, $response) use ($client) { $client->incr("app:requests"); $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello World\n"); };
Now, how to add a Redis server to our app? We can connect directly to a specific hostname, or assume that whoever works on our app has Redis installed and running. Both are somewhat fragile, and make setting up a development environment more difficult. There is a third option: installing Redis directly to the Docker image of our app. However, this would polute the image and make it less flexible.
Enter Fig, a tool to quickly build and start (isolated) development environments. Fig requires a single configuration file (default: fig.yml):
app: build: . links: - redis redis: image: redis
The above file defines two containers: app (which contains our php application), and redis. The first container depends on the latter through the “links” section. Fig will make sure that, within the app container, “redis” resolves and points to that container.
Save the file, and run:
$ fig -p app up
This should produce the following output:
Creating app_redis_1... Creating app_app_1... Attaching to app_redis_1, app_app_1 redis_1 | [1] 09 Nov 10:19:23.272 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf redis_1 | _._ redis_1 | _.-``__ ''-._ redis_1 | _.-`` `. `_. ''-._ Redis 2.8.17 (00000000/0) 64 bit redis_1 | .-`` .-```. ```\/ _.,_ ''-._ redis_1 | ( ' , .-` | `, ) Running in stand alone mode redis_1 | |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379 redis_1 | | `-._ `._ / _.-' | PID: 1 redis_1 | `-._ `-._ `-./ _.-' _.-' redis_1 | |`-._`-._ `-.__.-' _.-'_.-'| redis_1 | | `-._`-._ _.-'_.-' | http://redis.io redis_1 | `-._ `-._`-.__.-'_.-' _.-' redis_1 | |`-._`-._ `-.__.-' _.-'_.-'| redis_1 | | `-._`-._ _.-'_.-' | redis_1 | `-._ `-._`-.__.-'_.-' _.-' redis_1 | `-._ `-.__.-' _.-' redis_1 | `-._ _.-' redis_1 | `-.__.-' redis_1 | redis_1 | [1] 09 Nov 10:19:23.279 # Server started, Redis version 2.8.17 redis_1 | [1] 09 Nov 10:19:23.279 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect. redis_1 | [1] 09 Nov 10:19:23.279 * The server is now ready to accept connections on port 6379 app_1 | Server running at http://0.0.0.0:1337
By default, Fig will start the containers in the foreground, blocking the terminal. To start in the background, add ‘-d’ to the command line.
Docker & Vagrant
As of version 1.6, Vagrant directly supports Docker and Docker containers through a new provider. Additionally, on platforms where Docker is not natively supported (Mac, Windows), Vagrant automatically starts a Linux-based (VirtualBox) vm which can then run a Docker container.
Let’s create a Vagrantfile for our app. We can re-use the existing Dockerfile, and let Vagrant use that:
Vagrant.configure("2") do |config| config.vm.provider "docker" do |d| d.build_dir = "." d.ports = ["1337:1337"] end end
Save the file, and run ‘vagrant up’:
==> default: Deleting the container... ==> default: Removing built image... ==> default: Building the container from a Dockerfile... default: Image: d2717466cf97 ==> default: Fixed port collision for 22 => 2222. Now on port 2200. ==> default: Creating the container... default: Name: docker_blog_default_1415529103 default: Image: d2717466cf97 default: Volume: /home/user/dev/docker_blog:/vagrant default: Port: 1337:1337 default: default: Container created: 539ba7edfee50fa0 ==> default: Starting container... ==> default: Provisioners will not be run since container doesn't support SSH.
And once again, our app is available on port 1337, proxied to the container that Vagrant built.
Conclusion
If you’ve made it this far, you now know how to start a Docker container that runs your app, and link that container to other dependencies. Ofcourse this is but a little intro into the exciting world of Docker, which is rapidly changing and growing, and has much more to offer!