Building Minimal Docker Image for Rails App

It's no secret that we like to build things using Ruby here at Kontena, Inc. headquarters. And when it comes to web applications, Ruby on Rails is still one of the best frameworks out there. It’s great to use because it is a full web application stack with nice conventions and excellent tools.

But usually all the fun stops when it's time to ship that Rails application using Docker. Although I have to admit; nowadays it's relatively simple if you just pick official rails base image and customize it to your application needs. This convenience comes with a steep price: your application image is getting really fat since Rails base image starts at 772MB. Sometimes this is not acceptable. So how can we put our Rails app to diet?

Diet Recipe: Alpine Linux

Our goal is to create most minimal, but fully featured Rails application image. To achieve that we need to start from a really small base image. Alpine Linux fits that bill and as a bonus it comes with nice package manager. Alpine image is unbelievable tiny, weighting only 5MB.

For the demo purposes let's use ruby-rails-sample from Heroku.

Let's start to build Dockerfile by defining base image:

FROM alpine:3.2  

Then we need to update package manager index and install all necessary packages that our application needs to run. We combine these to single RUN command so that Docker does not need to create unnecessary image layers:

RUN apk update && apk --update add ruby ruby-irb ruby-json ruby-rake \  
    ruby-bigdecimal ruby-io-console libstdc++ tzdata postgresql-client nodejs

Next we add our Rails application Gemfile and Gemfile.lock.

ADD Gemfile /app/  
ADD Gemfile.lock /app/  

After Gemfiles are in place, we add one magical RUNcommand. First part adds all the build dependencies as a virtual group named build-dependencies. Second part installs bundler and runs bundle install command that installs all our application dependencies. After all gems are installed we finally remove virtual package group.

RUN apk --update add --virtual build-dependencies build-base ruby-dev openssl-dev \  
    postgresql-dev libc-dev linux-headers && \
    gem install bundler && \
    cd /app ; bundle install --without development test && \
    apk del build-dependencies

Why we need to combine all these commands to a single run clause? This trick ensures that virtual group (build-dependencies) packages does not grow image at all. The extra "fat" comes only from the installed gems.

Finally we add application sources to image, chown them to nobody user and switch container exec user also to nobody (because we don't really need root user here) and define default command to be executed when container starts.

ADD . /app  
RUN chown -R nobody:nogroup /app  
USER nobody

ENV RAILS_ENV production  
WORKDIR /app

CMD ["bundle", "exec", "unicorn", "-p", "8080", "-c", "./config/unicorn.rb"]  

Now the Dockerfile is done. See the complete Dockerfile below:

FROM alpine:3.2  
MAINTAINER jari@kontena.io

RUN apk update && apk --update add ruby ruby-irb ruby-json ruby-rake \  
    ruby-bigdecimal ruby-io-console libstdc++ tzdata postgresql-client nodejs

ADD Gemfile /app/  
ADD Gemfile.lock /app/

RUN apk --update add --virtual build-dependencies build-base ruby-dev openssl-dev \  
    postgresql-dev libc-dev linux-headers && \
    gem install bundler && \
    cd /app ; bundle install --without development test && \
    apk del build-dependencies

ADD . /app  
RUN chown -R nobody:nogroup /app  
USER nobody

ENV RAILS_ENV production  
WORKDIR /app

CMD ["bundle", "exec", "unicorn", "-p", "8080", "-c", "./config/unicorn.rb"]  

With our Dockerfile in place, we can build it:

$ docker build -t ruby-rails-sample .

After build is complete, we can check the resulted image size:

$ docker images | grep ruby-rails-sample
ruby-rails-sample                latest              9549afd8770e        1 minute ago       72.96 MB  

~73MB, not bad if compared to official Rails base image that can easily grow to over one gigabyte.

We are using these same practices to produce Docker images for our open source project Kontena - a new "developer first" Docker container orchestration platform written in Ruby. Although, our images are using more slimmer Ruby framework called Roda.

Image Credits: Rail by Victor Camilo