Dockerize your Development Env with Example
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.
- Dockerfile
 - docker-compose.yml
 - config/database.yml (partial)
 - docker-sync.yml
 - 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 . /myappdocker-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: trueconfig/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.
- Make sure your containers has started already.
 - Type 
docker psto check the app container name - Type 
docker attach {your_container_name}to attach to the docker process. - Now you can interact with Rails server process where you put 
binding.pry - Type 
<ctrl> + pthen<ctrl> + qto end the pry session. Do NOT do<ctrl> + cbecause 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!