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.
There are two ways to use Docker:
- An isolated ecosystem for your program sitting inside a container.
- 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 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
This is the base image it builds from. Many companies will want to pick official pre-designed images like
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
-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:
- Keep like things together.
- Make sure to clean up after yourself.
- 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.