Installing Private Gems During a Docker Build

docker ruby

EDIT: This post is no longer relevant since Docker have introduced an --ssh flag in recent versions. See this blog post for details.

If you want to install RubyGems using Bundler during a Docker build, life becomes a little bit more complicated when some of these Gems are hosted in a private Github repository.

The problem is that we do not want to expose any secret credentials in any of the container images that are built, and ideally we want to keep the build environment as simple as possible.

So what can we try?

SSH agent forwarding

Plenty of people have described this method, and it is the thing that pops up most when searching for solutions. It involves mounting your host ssh agent against the container you’re building:

Unfortunately if you’re using Docker for Mac, there is an open issue which means this solution turns into more of a hassle than you’d like.

Use a Gem server

It’s pretty trivial to run your own Gem server.

One solution could be to fetch the Gems you need in your build environment, push them to your own Gem server, and then configure your Docker build to fetch from this server.

I wasn’t a fan of this solution because it involves a lot of scaffolding within a build environment, and there were also complications in potentially making the repository available to other people working on the code.

Private Gem repository

Host your private Gems in a private repository such as Gemfury that allows you to pass in a token as an environment variable at build time.

If you’re already using a repository to host private Gems, I’d suggest this solution. We weren’t, and it felt like a lot of over engineering for what I was trying to solve.

Bundle package

I liked this solution, but it isn’t the most efficient, and may also contribute to a larger image size.

Run bundle package --all on the host to fetch the Gems. During the build, COPY them to the container as use bundle install --local.

If you’re building for a different platform, for example, building on MacOS for a Linux container, also use the --all-platforms flag.

This increases operation time as you must fetch on the host and copy to the container rather than fetching directly.

Use a Github personal access token

This is my preferred solution. You can pass in an environment variable without chaning too much in the build environment, but there are some considerations for the developers working in your repository.

Create a personal access token your Github account (or the account you’re using during build time). This can be passed in as the BUNDLE_GITHUB__COM environment variable and Bundler will use this token to authenticate with Github. The format of the value should be:

BUNDLE_GITHUB__COM=abcd0123generatedtoken:x-oauth-basic

In the Dockerfile, set this environment variable using ARG. Then pass it in using the --build-arg flag. This ensures that the value will not be present anywhere within the image, which ensures we can fetch the Gems securely. For example:

docker build -t myimage --build-arg BUNDLE_GITHUB__COM=mytoken:x-oauth-basic .

It’s worth noting you can just also just pass in an environment variable within that command:

export BUNDLE_GITHUB__COM==mytoken:x-oauth-basic
docker build -t myimage --build-arg BUNDLE_GITHUB__COM .

This will require ensuring that your Gemfile is fetching Gems using https rather than an SSH key. For example, where you once had:

'mygem', git: git@github.com:myrepo/mygem

You would now have:

'mygem', git: https://github.com/myrepo/mygem

If you were changing this in your Gemfile, it would force any developers to set a token as well. This can be added globally by running:

bundle config GITHUB__COM abcd0123generatedtoken:x-oauth-basic