Most of tutorials about dockerizing Ruby applications recommend to create a volume for storing bundles. This way gems can be cached and you don’t need to install all of them from scratch when Gemfile is updated. That’s good solution until you need to debug them (gems) - the code is not in your app directory so you are not able to add a breakpoint from your text editor In such case you have to login to the container and open the gem’s code there.

In this blog post I will show you an alternative solution which is much closer to what you use when you develop app without Docker.

Prerequisites

In this tutorial I’m assuming that you are using Docker with NFS file sharing. Personally I’m using Docker with xhyve driver. I haven’t tested this solution with VirtualBox sharing trough guest additions.

Building Docker image

We will start with Dockerfile:

// Dockerfile
FROM ruby:2.2-slim

RUN mkdir /app
WORKDIR /app

ENV BUNDLE_APP_CONFIG=/app/.bundle \
    BUNDLE_GEMFILE=/app/Gemfile \
    BUNDLE_JOBS=2 \
    BUNDLE_PATH=/app/vendor/bundle

ADD . /app

ARG USER_ID
ARG GROUP_ID
RUN useradd -u ${USER_ID} -g ${GROUP_ID} -rms /bin/bash app && \
      chown -R app:${GROUP_ID} /app

To make sure that file permissions on the host system are preserved during bundle install we are doing a small trick here - we are creating a user and assign him to the group with the same id that you have on the host system. We need to pass those ids as --build-arg option. Our docker-compose.dev.yml will propably looks like this (more or less):

version: '2'
services:
  web:
    build:
      dockerfile: Dockerfile.dev
      args:
        GROUP_ID:
        USER_ID:
    image: myapp
    volumes:
      - .:/app
    links:
      - postgres
    ports:
      - "3000"
    user: app
    command: bundle exec rails s -b 0.0.0.0
  postgres:
    image: postgres:latest
    ports:
      - "5432"
    volumes:
      - postgres-data:/var/lib/postgres
volumes:
  postgres-data:
    driver: local

In order to build it we call: GROUP_ID=$(id -g) USER_ID=$(id -u) docker-compose -f docker-compose.dev.yml build.

Btw. As a convention I’m using Dockerfile.dev and docker-compose.dev.yml file names to emphasize that they are for dev environment.

Running application

Before we run our app we need to add bundles to our app directory: docker-compose -f docker-compose.dev.yml run --rm web bundle install --without production

Now we are readt to start the app: docker-compose -f docker-compose.yml --remove-orphans up.

Summarize

As you can see with a little bit of overhead we are able to have our dockerized dev environment very close to what we have when we build app locally. Personally I’ve created bash functions and aliases to make docker builds and docker runs commands shorter - i.e:

# To build image
dbd image-name

# To start all containers
dup

# To run a command on web container
drun bundle install

The full source code for the Rails app can be found on my GitHub repo.

Small bonus

Script to open the gem’s directory in vim:

vim $(docker-compose -f docker-compose.dev.yml run --rm web bundle show rake | sed -e 's?\/app?'`pwd`'?' | tr "\\r" " ")

Just add it to your dotfiles and use it :).