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

151 lines
4.3 KiB
Markdown
Raw Normal View History

2023-02-05 21:07:24 -05:00
---
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!