Photo by kinsey on Unsplash

Ruby on Rails — Smaller docker images

Lemuel Barango
Jan 14 · 5 min read

When building docker containers, it is best practice to build small container images. Docker images that are smaller in size tend to have faster build times and pull times. Also, there are security benefits with using small docker images — small containers have a small attack surface compared to large docker containers.

How small should a rails docker container be? It depends. Every application is different. My rule of thumb is to keep container size to ~300MB (or ~100MB when compressed). It is easy for a fully featured rails application docker image to have a size of about 1GB! Here is an example:

I have created a new application using the http://jumpstartrails.com/ template

rails new myapp -d postgresql -m https://raw.githubusercontent.com/excid3/jumpstart/master/template.rb

Using a basic Dockerfile with a ruby:2.5.3 base image:

FROM ruby:2.5.1RUN apt-get update \
&& apt-get install -y apt-transport-https \
&& curl --silent --show-error --location \
https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key
add - \
&& echo "deb https://deb.nodesource.com/node_6.x/ stretch main"
> /etc/apt/sources.list.d/nodesource.list \
&& curl --silent --show-error --location
https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" >
/etc/apt/sources.list.d/yarn.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
postgresql-client nodejs apt-transport-https yarn \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /usr/src/app
ENV RAILS_ENV=production
ENV NODE_ENV=production
COPY Gemfile* package.json yarn.lock ./
RUN bundle install
RUN yarn install
COPY . .
RUN bin/rails assets:precompile
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]

We end up with a docker image of size:

REPOSITORY            TAG  SIZE
small-docker-images v1 1.48GB

So how do you keep the docker image size under control?

1. Use a smaller base image

In the first version, v1, we used ruby:2.5.1 as the base image which was very convenient but also large in size.

REPOSITORY   TAG     SIZE
ruby 2.5.1 869MB

Starting with a smaller base image can help reduce the size of the final image. An example of a smaller base image is ruby-alpine:

REPOSITORY   TAG            SIZE
ruby 2.5.1-alpine 45.3MB

That’s about 820MB removed!

Using an Alpine image as the base image could be a bit more involved, you have to run a few commands and install a few more packages:

FROM ruby:2.5.1-alpineRUN apk update \
&& apk upgrade \
&& apk add --update --no-cache \
build-base curl-dev git postgresql-dev \
yaml-dev zlib-dev nodejs yarn
WORKDIR /usr/src/app
ENV RAILS_ENV=production
ENV NODE_ENV=production
COPY Gemfile* package.json yarn.lock ./
RUN bundle install
RUN yarn install
COPY . .
RUN bin/rails assets:precompile
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]

After using an alpine base image, we end up with a small final v2 image that’s about half of v1

REPOSITORY            TAG  SIZE
small-docker-images v2 810MB

But we can reduce this even further!

2. Reduce the number of layers with multi-step docker builds

Just as the size of your git repo increases with the number commits/changes, the size of final docker image depends on the number of layers. You can reduce the number of layers by decreasing the number of RUN and COPY statements or by doing a multi-step build which is similar to a “git squash”.

Here’s my new Dockerfile using the multi-step build approach:

FROM ruby:2.5.1-alpine AS build-envARG RAILS_ROOT=/app
ARG BUILD_PACKAGES="build-base curl-dev git"
ARG DEV_PACKAGES="postgresql-dev yaml-dev zlib-dev nodejs yarn"
ARG RUBY_PACKAGES="tzdata"
ENV RAILS_ENV=production
ENV NODE_ENV=production
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"
WORKDIR $RAILS_ROOT# install packages
RUN apk update \
&& apk upgrade \
&& apk add --update --no-cache $BUILD_PACKAGES $DEV_PACKAGES \
$RUBY_PACKAGES
COPY Gemfile* package.json yarn.lock ./
RUN bundle config --global frozen 1 \
&& bundle install --path=vendor/bundle
RUN yarn install
COPY . .
RUN bin/rails assets:precompile
############### Build step done ###############FROM ruby:2.5.1-alpine
ARG RAILS_ROOT=/app
ARG PACKAGES="tzdata postgresql-client nodejs bash"
ENV RAILS_ENV=production
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"
WORKDIR $RAILS_ROOT# install packages
RUN apk update \
&& apk upgrade \
&& apk add --update --no-cache $PACKAGES
COPY --from=build-env $RAILS_ROOT $RAILS_ROOT
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]

And this reduced the size by about half!

REPOSITORY            TAG  SIZE
small-docker-images v3 360MB

This is around my ideal docker size but there is still a bit of cleaning up that can be done to reduce the size further.

3. Remove unnecessary files during the build

Some files that are used in development are never used in production, a few examples are:

1. app/assets vendor/assets node_modules - If you precompile your assets/webpack, these folders become irrelevant in prod2. tmp/cache spec are also not needed in production3. remove the gems cache and *.c and *.o files

After removing these files, this is my new docker file:

FROM ruby:2.5.1-alpine AS build-envARG RAILS_ROOT=/app
ARG BUILD_PACKAGES="build-base curl-dev git"
ARG DEV_PACKAGES="postgresql-dev yaml-dev zlib-dev nodejs yarn"
ARG RUBY_PACKAGES="tzdata"
ENV RAILS_ENV=production
ENV NODE_ENV=production
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"
WORKDIR $RAILS_ROOT# install packages
RUN apk update \
&& apk upgrade \
&& apk add --update --no-cache $BUILD_PACKAGES $DEV_PACKAGES $RUBY_PACKAGES
COPY Gemfile* package.json yarn.lock ./
# install rubygem
COPY Gemfile Gemfile.lock $RAILS_ROOT/
RUN bundle config --global frozen 1 \
&& bundle install --without development:test:assets -j4 --retry 3 --path=vendor/bundle \
# Remove unneeded files (cached *.gem, *.o, *.c)
&& rm -rf vendor/bundle/ruby/2.5.0/cache/*.gem \
&& find vendor/bundle/ruby/2.5.0/gems/ -name "*.c" -delete \
&& find vendor/bundle/ruby/2.5.0/gems/ -name "*.o" -delete
RUN yarn install --production
COPY . .
RUN bin/rails webpacker:compile
RUN bin/rails assets:precompile
# Remove folders not needed in resulting image
RUN rm -rf node_modules tmp/cache app/assets vendor/assets spec
############### Build step done ###############FROM ruby:2.5.1-alpine
ARG RAILS_ROOT=/app
ARG PACKAGES="tzdata postgresql-client nodejs bash"
ENV RAILS_ENV=production
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"
WORKDIR $RAILS_ROOT# install packages
RUN apk update \
&& apk upgrade \
&& apk add --update --no-cache $PACKAGES
COPY --from=build-env $RAILS_ROOT $RAILS_ROOT
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]

And the size comes to about

REPOSITORY            TAG  SIZE
small-docker-images v4 162MB

That’s pretty good considering the starting size of 1.5G.

Building small docker containers can improve performance in builds/pulls and security of the application. It’s easy to start reducing the size of your rails docker image by utilizing small base images and the multi-step build pattern.

Lemuel Barango

Written by

Software engineer at Fullscript https://fullscript.com https://github.com/lemuelbarango