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:
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
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 email@example.com 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