Docker & PHP: beyond virtual machines

(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:

React screenshot

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!

Michiel Rook

Michiel Rook is an experienced, passionate & pragmatic freelance coach, developer & speaker from the Netherlands. He loves helping teams and companies to develop better software and significantly improve their delivery process. When he’s not thinking about continuous deployment, DevOps or event sourcing he enjoys music, cars, sports and movies.

Leave a Reply

Your email address will not be published. Required fields are marked *