Sep 3rd, 2016

Ruby build pipeline with Docker

docker, build, artifacts, ruby

Docker all the things \o/

You have probably seen people running all kind of things inside containers. Anything really, like a famous UI editor or a Tor relay for example.

When it comes to a server stack, some people would like to see Docker containers running in production everywhere. It can be a great idea if your production infrastructure is designed to run containers.

What if you still run dedicated instances or bare metal machines? Would you even want to use it?

A realistic and proven use-case

Imagine your stack is composed of different OS versions, with apps running on various languages. Do you really want one build runner per OS? Do you want to setup every new build runner with all possible languages your dev team is using?

My friend and colleague Pierre explained really well in Captain Train's blog our current build process. Where Docker helps us to have:

Running all of your build pipelines - CI, CD, you name it - inside containers might be a good idea for you too. Try it out!

A bumpy ride

EDIT: The following paragraph solving a possible error with bundler has now been fixed in version 1.13.2 of bundler.

I am not trying to tell you it is plain easy and straightforward to build in containers. You will probably have some ajustements to make on the road. I will detail here the ride to build ruby apps inside a container.

Let's consider the target machine requirements to be ruby:2.3.1 and debian:8.

The app's build pipeline will be:

Our ruby app of choice for this example is rake.

Let's get started by describing our app environment.

# Dockerfile
# --
#
# Debian 8 Jessie base image (https://github.com/docker-library/ruby/blob/master/2.3/Dockerfile#L1)
FROM ruby:2.3

WORKDIR /app
# Make sure bundle config is kept in workdir
ENV BUNDLE_APP_CONFIG /app/.bundle/

# Our app
RUN bundle init
RUN echo "gem 'rake'" >> Gemfile
RUN bundle
RUN bundle --deployment

# How to run the app
ENTRYPOINT ["bundle", "exec", "rake"]

The build process is as simple as building the Docker image:

> docker build -t rake .
...

Let's run some tests on the app.

> docker run -p 8000:8000 \
> rake -rwebrick -e "WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: '.').start"
[2016-09-02 23:22:27] INFO  WEBrick 1.3.1
[2016-09-02 23:22:27] INFO  ruby 2.3.1 (2016-04-26) [x86_64-linux]
[2016-09-02 23:22:27] INFO  WEBrick::HTTPServer#start: pid=1 port=8000

> curl -I localhost:8000
HTTP/1.1 200 OK
Server: WEBrick/1.3.1 (Ruby/2.3.1/2016-04-26)
...

Everything is working as expected. Let's package our amazing app now:

> docker run --entrypoint tar rake --create . > app.tar

That's great because we now have our app packaged into a tar archive and ready to be deployed anywhere we want.

Time to get the app into production on our machine:

> tar --extract --file app.tar

> bundle exec rake -rwebrick -e "WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: '.').start"
bundler: failed to load command: rake
(/tmp/rake/vendor/bundle/ruby/2.3.0/bin/rake)
NoMethodError: undefined method `activate_bin_path' for Gem:Module
  /tmp/rake/vendor/bundle/ruby/2.3.0/bin/rake:22:in `<top (required)>'

Woupsy, the app won't start. What could possibly go wrong? I have packaged my app inside Docker it should just work!

Let's check the rubygems versions both on my machine and in the container:

> gem --version
2.5.1

> docker run  --entrypoint="gem" rake --version
2.6.7

Well in my case I was lucky enough to find a change in the way binstubs were generated by the latest rubygems codebase. So I tried to downgrade the version of the rubygems gem inside the container. A better way would obviously be to upgrade this gem on all the target machines but I wanted to share with you the possibility to change your rubygems version inside the base ruby Docker image.

ENV RUBYGEMS_VERSION 2.5.1
RUN echo $(gem which bundler) | (read r; echo ${r%gems*}) | xargs gem uninstall bundler -i
RUN gem update --system $RUBYGEMS_VERSION
RUN gem install bundler

The final container that would be able to build, test and package my ruby app is thus the following.

# Dockerfile
# --
#
# Debian 8 Jessie base image (https://github.com/docker-library/ruby/blob/master/2.3/Dockerfile#L1)
FROM ruby:2.3

WORKDIR /app
# Make sure bundle config is kept in workdir
ENV BUNDLE_APP_CONFIG /app/.bundle/
# Downgrade rubygems version
ENV RUBYGEMS_VERSION 2.5.1
RUN echo $(gem which bundler) | (read r; echo ${r%gems*}) | xargs gem uninstall bundler -i
RUN gem update --system $RUBYGEMS_VERSION
RUN gem install bundler

# Our app
RUN bundle init
RUN echo "gem 'rake'" >> Gemfile
RUN bundle
RUN bundle --deployment

# How to run the app
ENTRYPOINT ["bundle", "exec", "rake"]