Lessons learned from first attempt of dockerizing Ruby on Rails app

Docker impressed me since first time I’ve heard about it on local development conference. Few years passed while I’ve tried this by myself. Here is a story of my experience with Docker and related instruments.

As you may know, Docker is a software container platform and could be useful in three basic options: for local development environments, for apps running in isolated containers, for entire infrastructure built on top of containers clusters with microservices approach. Nowadays every option has a place for application but for me the second and the third looked like the most interesting.

Let’s dockerize it!

My idea for Docker application was to create an image for APIQ CMS system that our team open sourced few months ago. I’ve started with simple Dockerfile that was pretty common for any Rails project and could be found on Docker Hub. It looked really simple but it didn’t work in my case. If your project is a little bit more complex, things go wrong and hard.

Lesson 1: gems with C bindings cause problems

APIQ has a dependency on well-known therubyracer (and actually libv8) gem. If you ever worked on dockerizing Rails app with such dependency, you know what I’m talking about. After spending some time on googling, I’ve came to this:

ENV LIBV8_VERSION 3.16.14.18
...
RUN apk --update --no-cache add --virtual build-deps build-base python postgresql-dev nodejs g++; \
bundle config build.libv8 --enable-debug && \
LIBV8_VERSION=$LIBV8_VERSION bundle install --without development test && apk del build-deps

Lesson 2: embed assets in image or precompile

Precompiling assets with Rails Assets pipeline can take a lot of time. You can keep your image as small as possible and not pack assets into image (but then you need to call assets precompiling task on launching concrete container). I’ve chosen to precompile them on image build stage but faced with another problem: lack of database :-) Yes, this is well-known fact that rake (rails) tasks load entire environment so you need a database. I’ve found a workaround in using activerecord-nulldb-adapter gem that does the trick.

RUN bundle exec rails kms:install:migrations \   
&& SECRET_TOKEN="$(bundle exec rails secret)" DB_ADAPTER=nulldb bundle exec rails assets:precompile

As you may notice, you’ll need some stub for SECRET_TOKEN as well. Your database.yml could look like this then:

default: &default  
adapter: <%= ENV['DB_ADAPTER'] ||= 'postgresql' %>
...

Lesson 3: image size rage

If you’re going to have your Docker image as small as possible, you’d like to try these options:

  • choosing the right base image (for example, based on Alpine Linux ruby:alpine)
  • minimizing number of intermediate layers (by concatenating RUN commands installing necessary packages especially)
  • installing only really necessary OS packages

Lesson 4: Docker for Mac

As I was on the way of building my image, I’ve noticed my disk space is going to zero after several build attempts. I was totally surprised that this was a Docker for Mac issue that is still not resolved (https://github.com/docker/for-mac/issues/371). The simplest working solution for me was resetting Docker to factory defaults.

And finally… Going public

After all these things that I’ve learned when was trying Docker, I’ve finally pushed new image to Docker Hub. And that was the easiest step: you just need to wait until image will get uploaded!

I hope these lessons will help anyone faced with similar problems to save their time and continue digging into the world of Docker. :-)