In the previous post, I wrote the basics and walkthrough on how to dockerize your local development environment. After that, I was requested to write a more concrete example, so this is a post with actual example from one of my personal rails projects.

For this application, I don’t use Docker for production (yet) so it is relatively simple to start with, becasuse I didn’t have to worry about production impact.

In case you haven’t yet, I recommend you check out previous post frist, because it will give you necessary technical backgrounds to understand each section below.

[Edit] One of my coworkers pointed out that we can use named volume for DB persistence instead of docker-syncing it. I will update the post once I confirm the updated setting works. [Edit2] I have been choosing setting up a DB on host and use host.docker.internal when doing so is not difficult and there have been less pains.

Files

These files and the setup work for MacOS X, High Sierra and Mojavi. I haven’t confirmed any other version of MacOS. And I haven’t changed anything for publication purpose. So these are the real files I use.

  1. Dockerfile
  2. docker-compose.yml
  3. config/database.yml (partial)
  4. docker-sync.yml
  5. setup_local_docker.sh

Dockerfile

Here’s my Dockerfile. I said it is not used in production, but it’s still nice to consider production usage as a future possibility. That’s why I used BUILD_ENV here. I added git and less for dev environment. less is for a better paging on rails console.
Just in case the base image ruby:2.5.3 is Ubuntu based.

FROM ruby:2.5.3
ARG BUILD_ENV=production

RUN apt-get update -qq && \
  apt-get install -y build-essential libpq-dev mysql-client
RUN if [ "$BUILD_ENV" = "dev" ]; then apt-get install -y git less; fi
### in need for yarn install
RUN apt-get remove -y cmdtest yarn
RUN curl -sL https://deb.nodesource.com/setup_11.x | bash -
RUN apt-get install -y nodejs
RUN npm install -g yarn
###
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

docker-compose.yml

Here’s my docker-compose.yml. You should rename this to something like docker-compose.local.yml when you have to use docker-compose for production, which is not the case for me. I also expose 13306 to the host machine, so I can easily interact with docker based mysql data. “volumes” are the ones managed by docker-sync, which protects you from the hell of slowness of your local docker. The options stdin_open and tty are for “binding.pry” which I will explain later.

version: '3'
services:
  db:
    image: mysql:5.7
    volumes:
      - db-sync:/var/lib/mysql:nocopy
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
    ports:
      - "13306:3306"

  app:
    build:
      context: .
      args:
        BUILD_ENV: dev
    volumes:
      - app-code-sync:/myapp:nocopy
    ports:
      - "3000:3000"
    depends_on:
      - db
    env_file:
      - .env
    command:
      bash -c "bundle install && rm -f tmp/pids/server.pid && bundle exec rails s -b 0.0.0.0 -p 3000"
    stdin_open: true # This is for binding.pry
    tty: true # This is for binding.pry
    environment:
      DOCKER_DB: 'mysql2://root:@db/slang2_development'

volumes:
  app-code-sync:
    external: true
  db-sync:
    external: true

config/database.yml (partial)

Since we need to provide docker MySQL host, I put DOCKER_DB as an environment variable. Here’s a change I made so it tries to picks up the env var when exists.

...
development:
  <<: *default
  url: <%= ENV['DOCKER_DB'] || Rails.application.credentials.db_url %>
...

docker-sync.yml

Like I mentioned in the previous post, without using docker-sync, the performance is atrocious. Docker’s solution of using cache or delegate does not help much at the point of this writing.

I have 2 volumes, one is for application source code, the other one is database data. It’s OK if you don’t volume-mount the database but docker-compose down will wipe your data, so please keep that in mind.

version: '2'

options:
  verbose: false
syncs:
  app-code-sync:
    src: './'
    sync_strategy: 'native_osx'
    sync_excludes: ['.git/*', 'log/*', '*_history']
    sync_excludes_type: 'Name'
    sync_userid: '1000'
  db-sync:
    src: './tmp/db'
    sync_strategy: 'native_osx'

setup_local_docker.sh

And I have a bash script that glues the whole build and preparation process for my local Docker env. By running this command, even on a new MacOS machine, I can recreate my local docker environment many times.
Don’t forget to do chmod +x before running ./setup_local_docker.sh

#!/usr/bin/env bash
set -e

function exit_with_error() {
  if [ $# -eq 1 ]; then
    echo >&2 "ERROR: $1"
  fi
  echo >&2 'Exiting...'
  exit 1
}

# This is in case you use "url" in config/database.yml
read -p 'Enter your development database name: ' db_name
if [[ -z "$db_name" ]]; then
  exit_with_error 'No database name specified.'
fi

echo 'Building your image(s)...'
# if you share Dockerfile between production and dev, this build-arg will be useful
docker-compose build --build-arg BUILD_ENV=dev

echo 'Set up docker-sync'
gem install docker-sync # It might be OK to bundle this under development group, too
docker-sync clean && docker-sync start

echo 'Preparing rails and database...'
docker-compose run --rm app bash -c \
   "mysql -h db -u root -e 'CREATE DATABASE IF NOT EXISTS $db_name;' && mkdir -p /app/tmp/pids && rails db:migrate"

echo 'Local docker setup succeeded'
echo 'run "docker-compose up" or "docker-compose -d"'
echo 'Bash access is by "docker-compose exec app bash"'

How to do “binding.pry”

During the development, binding.pry is useful. Here’s how you can do it with local docker.

  1. Make sure your containers has started already.
  2. Type docker ps to check the app container name
  3. Type docker attach {your_container_name} to attach to the docker process.
  4. Now you can interact with Rails server process where you put binding.pry
  5. Type <ctrl> + p then <ctrl> + q to end the pry session. Do NOT do <ctrl> + c because that would stop the container process itself.

For a little bit more complicated projects

My personal project is really simple so there is just one app container but I believe people do something more complicated. I actually dockerized Rails application with a sidekiq container (at work), too. I had to have sidekiq under services which is very similar with app section, but I don’t think it’s difficult for you to figure it out on your own :)

Happy local development with Docker!