Rails Development On Docker – Part 2

Last time we talked about using docker-machine in the first part of our three part series, but that only gets you to the house. Now we have to show you the (metaphorical) paintbrush and ladder so your team can define the perfect environment.

docker-compose

There are two ways to use Docker:

  1. An isolated ecosystem for your program sitting inside a container.
  2. A series of ecosystems that are interconnected, with a service per container.

I think the future and mature way to use Docker is the latter. If like me you agree then you’re going to need docker-compose. Not only is it a tool for managing the web of containers it’s also the gateway for the docker command. Anything the docker command can do so also can the docker-compose, but with one small caveat: Most of the time you have to specify the container name. For example, these two commands are equivalent:

$ docker run /bin/bash
$ docker run web bin/bash

Where did the container name web come from? Well it’s defined in our docker-compose.yml configuration file:

web:
  command: bin/rails server --port=$PORT --binding=$binding
  volumes:
    - /usr/src/application:/usr/src/application
  build: .
  env_file: .env.web
  ports:
    - "3000:3000"
  links:
    - postgres
    - memcached
postgres:
  image: postgres:9.4
  ports:
    - 5432
memcached:
  image: memcached:1.4
  ports:
    - 11211

Let’s break this beast down into smaller parts.

The Container Definition

web:
  # ...
postgres:
  # ...
memcached:
  # ...

The root keys of this document are all container names. Here I’ve defined three specific containers that will be set as environment variables:

APPLICATION_MEMCACHED_1_PORT=tcp://172.17.0.2:11211
MEMCACHED_PORT=tcp://172.17.0.2:11211
MEMCACHED_1_PORT=tcp://172.17.0.2:11211
APPLICATION_POSTGRES_1_PORT=tcp://172.17.0.3:5432
POSTGRES_PORT=tcp://172.17.0.3:5432
POSTGRES_1_PORT=tcp://172.17.0.3:5432

There are going to be a ton more of these, but there are the important values. You’ll notice there seem to be duplicates. This is due to the nature of containers. You might want to scale up your PostgreSQL containers so you have 5 at the same time. The way you would programmatically differentiate between them is via these values.

Further you’ll find the /etc/hosts file has been mutated and they include some great shortcuts:

$ cat /etc/hosts
172.17.0.5  6896242e97ed
127.0.0.1  localhost
::1  localhost ip6-localhost ip6-loopback
fe00::0  ip6-localnet
ff00::0  ip6-mcastprefix
ff02::1  ip6-allnodes
ff02::2  ip6-allrouters
172.17.0.3  postgres 0db19fc810d1 application_postgres_1
172.17.0.3  postgres_1 0db19fc810d1 application_postgres_1
172.17.0.2  application_memcached_1 1bac9dfc3096
172.17.0.3  application_postgres_1 0db19fc810d1
172.17.0.2  memcached 1bac9dfc3096 application_memcached_1
172.17.0.2  memcached_1 1bac9dfc3096 application_memcached_1
172.17.0.3  application_postgres_1
172.17.0.3  application_postgres_1.bridge
172.17.0.5  application_web_run_3
172.17.0.5  application_web_run_3.bridge
172.17.0.2  application_memcached_1
172.17.0.2  application_memcached_1.bridge

This allows you to make very simplistic connection definitions:

default: &default
  adapter: postgresql
  encoding: unicode
  pool: 5
  username: postgres
  host: postgres

The other important key here is the volume: key, as it describes the mounting of the intermediary machine (or host) to the guest machine (the container):

volumes:
  - /usr/src/application:/usr/src/application

docker

docker is the tool for manipulating the containers. It gives you programatic access to containers, instructions for building those containers, and details on how the outside world communicates with those containers. All of this is designed in a Dockerfile file. You can see ours here:

FROM debian:jessie

ENV DEBIAN_FRONTEND noninteractive
ENV SOURCE "/usr/src/..."
WORKDIR $SOURCE

# Installing build dependencies
RUN echo "Installing build dependencies" \
  # Updating the apt-get index
  && apt-get update \
  # Grabbing the core libraries
  && apt-get install -y --no-install-recommends \
    git \
    libmagickcore-dev \
    libmagickwand-dev \
    libpng-dev \
    libpq-dev \
    postgresql-client-9.4 \
    libqt5webkit5-dev \
    qt5-default \
  # Cleaning up apt lists cache
  && rm -rf /var/lib/apt/lists/*

# CRuby Setup
RUN echo "Installing CRuby" \
  && apt-get update \
  && apt-get install -y --no-install-recommends ruby2.1

# Node.js Setup
RUN echo "Installing Node.js" \
  && apt-get update \
  && apt-get install -y --no-install-recommends nodejs

There are four parts to this file that are important so we’ll quickly go through that detail now:

The FROM Instruction

FROM debian:jessie

This is the base image it builds from. Many companies will want to pick official pre-designed images like ruby:2.2 or node:3.0. You might be tempted to use the latest value for the version, but this is a mind killer. An external source mutates an index and suddenly you’re building from scratch. We personally chose to go with a bare bones approach as we wanted to get some insight into the process. The only flaw with FROM in my opinion is that you can’t compound multiple to form a chain of composition.

The ENV Instruction

ENV DEBIAN_FRONTEND noninteractive
ENV SOURCE "/usr/src/..."
WORKDIR $SOURCE

Here you’ll define some environment variables for the build process. As discussed in the docker-compose section you’ll actually want to define application specific environment variables using the env-file keyword, but those values aren’t present until docker build is finished. We’ve included these to have tighter control over our build process. The WORKDIR isn’t an environment variable, but it’s basically the same idea just for docker’s target directory purposes.

The RUN Instruction

# Installing build dependencies
RUN echo "Installing build dependencies" ...

# CRuby Setup
RUN echo "Installing CRuby" ...

# Node.js Setup
RUN echo "Installing Node.js" ...

This is the meat of the Dockerfile and where most of your pre-application directory setup should go. In most companies you’ll probably see a lot to do with bundle install or similar. Due to the constraints of VOLUME vs -v/--volume (they aren’t the same thing) we’ve opted to only do pre-sync operations here. You can learn about RUN in other tutorials but here’s what I’ll suggest:

  1. Keep like things together.
  2. Make sure to clean up after yourself.
  3. Don’t be afraid to compile from source.

This is the second post in this series and we plan on having many more so thanks for reading! If this sort of thing is something you would enjoy working on we are looking for awesome engineers to join our team.


Docker and the Docker logo are trademarks or registered trademarks of Docker, Inc. in the United States and/or other countries. Docker, Inc. and other parties may also have trademark rights in other terms used herein.