Want to see the full-length video right now for free?
Sign In with GitHub for Free AccessOn this week's episode, Chris is joined by thoughtbot CTO Joe Ferris to discuss Docker, the open platform for building and running containerized applications.
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.
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!
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.
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.
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.
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/
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.
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
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
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
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.
While Docker offers an array of benefits, there are a handful of complications or pitfalls to watch out for.
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.
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.