What is Docker

In short, Docker enables you to install and run software in isolated containers. It provides a way to layer images. For instance, you could have a PostgresSQL image which is based on an Ubuntu 14.04 image. Containers can talk to each other via internal or external ports. This means, you could have two containers, one for the database and one for your application, both running on the same machine but not seeing each other except through the ports you expose.

Application-level virtualization is way more efficient than machine-level virtualization like VMware and OpenBox do it. IBM published a paper this year confirming this. The following slide visualizes this fact pretty good:

For a full introduction, please watch this excellent intro video:

Terminology

  • Image: Images are typically built through a Dockerfile and can be layered and reused.
  • Container: Containers are started images. Thus, when started, they contain all file which exist in the image but can create or delete files during runtime.

Think of images as an immutable DVD with application on it, and the container is the running application in memory. Something like that.

How we use docker

We use fig as layer above docker. fig enables us to easily build and bind our services together and start them in isolated docker containers.

For instance, this is a fig.yml file we use in a project:

db:
  image: orchardup/postgresql
  ports:
    - "5432"
web:
  image: rweng/ini-rub-de-web
  command: /bin/bash -l -c "deploy/start_services.sh"
  volumes:
    - .:/var/www
    - ./log:/var/volume1/log
  ports:
    - "80:80"
  links:
    - db
  environment:
    RAILS_ENV: 
    INI_DB_USERNAME: docker
    INI_DB_PASSWORD: docker
test:
  image: rweng/ini-rub-de-web
  command: /bin/bash -l -c "deploy/test_setup.sh"
  volumes:
      - .:/var/www
      - /var/www/public/uploads
  links:
      - db
  environment:
    RAILS_ENV: test
    INI_DB_USERNAME: docker
    INI_DB_PASSWORD: docker

As you can see, we define three containers we use: db, web and test. All three are based on a certain image. If an image with this name exists, fig uses it to start a container of it. If it does not exist, it looks at https://hub.docker.com/ if an image with this name has been published there.

It is also possible to use build: ./ instead of image: rweng/ini-rub-de-web. In this case, fig will build an image based on the Dockerfile in the given directory, in the case above ./.

We like to have one image, built once and used everywhere, to ensure the same foundation on all machines. Thus, we build our image with docker build -t rweng/ini-rub-de-web ./ and then push it to the docker hub with docker push rweng/ini-rub-de-web. Since we run bundle install when a container starts, rebuilding the image is only required when we add system packages like redis, or when so much time has passed that the base image is totally outdated so that starting a container of it (running bundle install) takes too long.

This is how our Dockerfile looks like:

#
# Dockerfile to install Rails Stack
#
# This file must be in the root of the rails app due to add command below
# see https://github.com/dotcloud/docker/issues/2745
#
# VERSION 2 - EDITION 1

FROM ubuntu:14.04
MAINTAINER Robin Wenglewski, robin@wenglewski.de

# install required packages
RUN apt-get update && apt-get install -y \
    curl \
    git \
    nginx \
    libpq-dev \
    imagemagick \
    libmagickwand-dev \
    nodejs \
    vim-nox \
    phantomjs

# Setup nginx
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf
RUN mkdir /etc/nginx/ssl
ADD deploy/default /etc/nginx/sites-available/default

# install rvm
RUN curl -sSL https://get.rvm.io | bash -s stable
RUN echo 'source /usr/local/rvm/scripts/rvm' >> /etc/bash.bashrc

# install ruby and gems
RUN /bin/bash -l -c "rvm install --default 2.0.0"
RUN /bin/bash -l -c "gem install bundler"

ADD . /var/www
WORKDIR /var/www
RUN /bin/bash -l -c bundle install

You can read about the way the Dockerfile works in the docker documentation.

As you can see in the fig.yml above, we pass in the database credentials via environment variables.

Our config/database.yml uses these:

development: &DEFAULT
  adapter: postgresql
  encoding: unicode
  database: ini_development
  pool: 5
  username: <%= ENV.fetch('INI_DB_USERNAME', 'root') %>
  password: <%= ENV.fetch('INI_DB_PASSWORD', '') %>
  host: <%= ENV.fetch('DB_1_PORT_5432_TCP_ADDR', 'localhost') %>
  port: <%= ENV.fetch('DB_1_PORT_5432_TCP_PORT', '5432') %>

fig automatically creates environment variables of exposed ports in other containers. Thus, the following lines in our fig.yml make the variables DB_1_PORT_5432_TCP_ADDR and DB_1_PORT_5432_TCP_PORT available in our test and web containers:

ports:
    - "5432"

Note that this is only internally exposed, so other containers can connect to port 5432 but not other machines. To expose ports externally use a complete mapping as we've done with the web container:

ports:
    - "80:80"

To start the db and web container, we only have to run fig up -d db web. This pull the images for both containers from the docker hub, if they are not already present. Thereafter, a container is started based on these images and the specified command: is executed. In out web container, this is deploy/start_services.sh:

#!/usr/bin/env bash

# this script is executed once the web container started

echo "running start_services.sh in RAILS_ENV=$RAILS_ENV"

bundle install

cp config/database.sample.yml config/database.yml
rake tmp:create
rake tmp:clear
rake db:create
rake db:migrate
rake db:seed

if [ "$RAILS_ENV" = "production" ]; then
    rake assets:precompile
else
    rm -rf public/assets
fi
echo "assets precompiled"

bundle exec unicorn -c config/unicorn.rb -D
echo "Started unicorn"

echo "starting nginx, finshed"
nginx

The RAILS_ENV is passed into the web container from the host because no value was provided. Compared to the test container, where we set it explicitly to test.

Testing

We run fig up test to let the test suite run through once, or fig run test /bin/bash which gets us into the container where we can run rspec or guard as we please.

Tools around Docker

Drone

Drone is a Continuous Integration platform built on Docker

Drone can test and build your application and even deploy it when built successfully. It is extremely easy to get started with drone since you basically only have to configure your environment.

Dokku

Dokku is build on docker and provides a mini-version of Heroku. Once started, you can push your applications to your machine which run in isolated containers.

However, Dokku only runs on a single host. The founder of Dokku, who also also worked / works(?) on docker, then started Flynn.

From our experiments, Dokku is not production ready. So we are sticking with plain Docker + fig.

Flynn

Flynn is not yet production ready, but it already looks awesome. It basically consists of two layers:

  1. a cluster layer which enables you to scale and distribute your applications
  2. multiple, independent services that enable you to push/deploy your applications, provide an API to manage Flynn, build and use heroku-like slugs to run your application, etc

In summary, Flynn is on a great path and I am looking forward to when it is production ready.

Additional Resources

Summary

Our process is certainly not perfect yet. However, we are sure of the potential of the Docker ecosystem and are already profiting from it.

I'm looking forward to reading about your experiences with and impressions of Docker in the comments.