Video

Want to see the full-length video right now for free?

Sign In with GitHub for Free Access

Notes

On this week's episode, Chris is joined by thoughtbot CTO Joe Ferris to discuss Docker, the open platform for building and running containerized applications.

What is Docker?

Docker is a platform built to support working with containers for isolating dependencies and processes.

Containers are an alternative technology to virtual machines. With VMs, you end up having the entirety of the operating system duplicated and running for each of the virutal machines. Containers on the other hand are a more lightweight approach, sharing the kernel and other lower level pieces, while isolating the processes and file system. If you use Heroku then your code is already running in a container that they happen to call a Dyno.

Docker is an attempt to standardize and automate working with containers to make it straightforward for both local development and deployment.

Be sure to check out the Docker user guide for a deeper dive into what Docker is and where it can fit in your development workflow.

Why Use Docker?

Docker and the underlying container technology have a host of benefits that make them interesting for both local development and production deployment.

With Docker and containers it becomes possible to lock in all dependencies and services used by your app, and ensure that your development environment matches production. With Docker you can configure not only things like Ruby and Rails versions, but even which version of Postgres you're running.

In addition, Docker automates the setup and configuration of all the different services needed to run an application. Gone are the days of "works on my machine!", and now getting started on a new project, even one with a complex set of services, is as simple as docker build and docker up.

One final feature is the idea that Docker containers are the by-product of executing the instructions in the Dockerfile and docker-compose.yml (see below for more detail on these), and as such we have an artifact of executable documentation for running our application. How handy!

Using Docker Directly

To start, we'll walk through the process of getting the Upcase app up and running with Docker using direct Docker commands. Later we'll demonstrate a more streamlined approach, but for now it's good to get a sense of the underlying actions needed to run the app via Docker.

Pulling Images

The first step is to pull down a base image. Base images act as starting points and typically contain needed dependencies like Ruby, Haskell, or Postgres.

Images are stored in a registry. By default, images are pulled from Docker Hub which is the core registry, but it's possible (and easy) to run a private registry for your organization and pull from there instead.

To pull down an image, we can run a command like:

$ docker pull ruby:2.2.2

With this, Docker will pull down the 2.2.2 tagged version of the ruby image from the Docker hub registry.

Running Processes

Now that we have a Docker image locally, we can use it to run a container.

$ docker run -it ruby:2.2.2 irb

This command instructs Docker to run a container, making it interactive with the -i flag (by default they are not interactive), specifying the image to base the container on with -t ruby:2.2.2, and finally specifying the command of irb to run.

It's worth noting that although a lot of container magic happens when we run this command, it has surprisingly little overhead and feels almost as if we're running irb directly in our local machine rather than in the container.

Configuring with the Dockerfile

The Dockerfile is a file that defines the recipe for building images. It specifies things like what base image to start from, any installation commands to run, and more specific commands like copying in the source code for your project.

This is the Dockerfile for the Upcase Docker image used in this video:

FROM ruby:2.2.2
RUN apt-get update -qq \
  && apt-get install -y --no-install-recommends \
  libpq-dev \
  qt5-default \
  libqt5webkit5-dev \
  nodejs

RUN mkdir /app
WORKDIR /app
COPY Gemfile /app/
COPY Gemfile.lock /app/
RUN bundle install

COPY . /app/

Running on OS X

Although Docker can run comfortably on any Linux variant, it cannot run directly on OS X. Instead, you need to use another tool called boot2docker which runs your Docker server and containers in a Vagrant virtual machine. Although this seems like a ton of additional machinery to require in order to run a simple app, in reality boot2docker and Vagrant stay out of your way and introduce almost no noticeable overhead or complexity into working with Docker.

Note There is an alternative to boot2docker that is being worked on now called Docker Machine. Docker Machine is intended to be a more general solution to configuring a Docker host and will likely replace boot2docker soon.

Building a Docker Image

docker build is the command used to execute the commands listed in the Dockerfile and produce a new image. Specifically, to build an image based on the Dockerfile shown above for the Upcase repo we can run:

$ docker build .

This will build an image called upcase, and we can run a container based on this image with:

$ docker run -it upcase bin/rails server

Connecting Containers

We've now progressed far enough with our base Upcase image that the Rails app is attempting to connect to a postgres database server, but since our container is isolated, it can't find a server and Rails crashes.

Instead, we can pull down a postgres image, run that, and connect the two images by name:

$ docker run -d --name db -t postgres: 9.4

This first command will start up a postgres container, putting it in the background with the -d flag.

Next we can run the upcase container, passing the --link=db flag to connect it to our running postgres db container:

$ docker run --link=db -it upcase bin/rails server

Exposing Ports

The final step needed to view the app in our local browser is to map the application port out of our container. We need to map port 3000 from the container to port 3000 on our machine, as well as binding to 0.0.0.0 rather than localhost as localhost would be local to the container.

$ docker run --link=db -it upcase -p 3000 bin/rails server -b 0.0.0.0

Docker Compose

Rather than running all of these commands directly and manually connecting the various containers needed to run an application, we can use docker-compose.

docker-compose is another tool designed to be used alongside docker that makes defining, connecting, and running multi-container applications more convenient. It uses a yaml configuration file to define the containers we want to run, and configurations such as base image, port mappings, and command that we need for each.

The following is the docker-compose.yml used in the video to configure the Upcase application.

db:
  image: postgres:9.4
  ports:
   - "5432"
web:
  build: .
  command: rails s -b 0.0.0.0
  volumes:
    - .:/app
  stdin_open: true
  ports:
   - "3000:3000"
  links:
   - db

From there, to get the application running we simply need to build it and boot it up:

$ docker-compose build
$ docker-compose up

So much simpler!

Adding new services and processes to our application fleet is as simple as adding a new block to the yaml config file. Similarly, getting a new contributor up and running on a project is extremely simple and no longer involves afternoons of fiddling. Think of how nice it was when Bundler came along to help manage gem dependencies, but multiply that out over everything in your application.

Drawbacks

While Docker offers an array of benefits, there are a handful of complications or pitfalls to watch out for.

  1. The isolation can be confusing at first.

Concepts like mapping ports and not having direct access to the file system can be frustrating, but the learning curve on them is quick enough that they are not a major issue.

  1. Tools like pry and save_and_open_screenshot are more difficult to use as you have to break through the wall of isolation that the container imposes.

Knowing that Docker is a relatively young technology, these are likely to be solved and smoothed over in the near future as more developers adopt Docker into their workflow.