atomaka.com/_posts/2017-03-01-a-more-flexible-dockerfile-for-rails.md

150 lines
4.3 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
layout: post
title: A More Flexible Dockerfile for Rails
tag:
- rails
- docker
- devops
---
One of my primary motivations for working with [Docker](https://www.docker.com/)
was creating a single artifact that I could toss into any environment. It has
been fantastic at this. I can throw together a simple Dockerfile that will build
my [Rails](http://rubyonrails.org/) application as an image for production in
about five minutes.
```
FROM ruby:2.3-alpine
ADD Gemfile* /app/
RUN apk add --no-cache --virtual .build-deps build-base \
&& apk add --no-cache postgresql-dev tzdata \
&& cd /app; bundle install --without test production \
&& apk del .build-deps
ADD . /app
RUN chown -R nobody:nogroup /app
USER nobody
ENV RAILS_ENV production
WORKDIR /app
CMD ["bundle", "exec", "rails", "s", "-b", "0.0.0.0", "-p", "8080"]
```
Except now that when I need to run the applications test suite, I do not have
the dependencies I need. That Dockerfile might look something like this.
```
FROM ruby:2.3-alpine
RUN apk add --no-cache build-base postgresql-dev tzdata
ADD Gemfile* /app/
RUN cd /app; bundle install
ADD . /app
RUN chown -R nobody:nogroup /app
USER nobody
WORKDIR /app
CMD ["bundle", "exec", "rails", "s", "-b", "0.0.0.0", "-p", "8080"]
```
Many people decide to include both of these Dockerfiles in their repository as
Dockerfile and Dockerfile.dev. This works perfectly fine. But now we have a
production Dockerfile that never gets used during development. Of course, it is
going through at least one staging environment (hopefully) but it would be nice
if we had a little more testing against it.
Much like Docker provides us the ability to have a single artifact to move from
system to system, I wanted to have a single Dockerfile shared between all
environments. Luckily, Docker provides us with
[build arguments](https://docs.docker.com/engine/reference/builder/#/arg). A
build argument allows us to specify a variable when building the image and then
use that variable inside our Dockerfile.
In our current Rails Dockerfile, we have two primary differences between our
environments:
- The gem groups that are installed
- The environment that the application runs as
Bundlers
[BUNDLE_WITHOUTBUNDLE_WITHOUT](http://bundler.io/man/bundle-config.1.html#LIST-OF-AVAILABLE-KEYS)
allows us to specify the gem groups to skip via an environment variable making
both of these resolvable through environment configuration. Using this, our
shared Dockerfile could look like this:
```
FROM ruby:2.3-alpine
ARG BUNDLE_WITHOUT=test:development
ENV BUNDLE_WITHOUT ${BUNDLE_WITHOUT}
ADD Gemfile* /app/
RUN apk add --no-cache --virtual .build-deps build-base \
&& apk add --no-cache postgresql-dev tzdata \
&& cd /app; bundle install \
&& apk del .build-deps
ADD . /app
RUN chown -R nobody:nogroup /app
USER nobody
ARG RAILS_ENV=production
ENV RAILS_ENV ${RAILS_ENV}
WORKDIR /app
CMD ["bundle", "exec", "rails", "s", "-b", "0.0.0.0", "-p", "8080"]
```
The secret sauce here is `ARG BUNDLE_WITHOUT=test:development`. Running
`docker build -t rails-app .` will use the default value provided for the
`BUNDLE_WITHOUT` build argument, test:development, and a production Docker image
will be built. And if we specify the appropriate build arguments, we can
generate an image suitable for development.
```
docker build -t rails-app --build-arg BUNDLE_WITHOUT= --build-arg RAILS_ENV=development .
```
will generate our Docker image with all test and development dependencies
available. Typing this for building in development would get pretty tedious so
we can use docker-compose to make it easier
```
version: '2'
services:
app:
build:
context: .
args:
- BUNDLE_WITHOUT=
- RAILS_ENV=development
links:
- database
ports:
- "3000:8080"
env_file:
- .env
volumes:
- .:/app
tty: true
stdin_open: true
```
Now, `docker-compose up -d` is all we need in development to both build and
launch our development image.
Finally, we have a single Dockerfile that can be used to build an image for any
of our applications needs. Of course, there are some trade-offs. For example,
build time in development will suffer in some cases. But I have found only
maintaining a single Dockerfile to be worth these costs.
Have another way to deal with this issue? Please share!