The requirements for local Docker is different from production. Also there are unique difficulties of running Docker on your local, too. This post is about what I learned so far regarding how to use Docker comfortably for our daily development after trial and error.
(Edit) I added a link to real files I use at the bottom of this post, but first please read through this!

First I’d like to mention some assumptions here.

  1. You do daily development on your local machine
  2. Nowadays you are likely to use Docker for production through some sort of container services such as AWS ECS, so docker-compose is only for local (and/or non-production) environment.
  3. This post is about MacOS X as your local host, although you should be able to have local Docker setup on other platforms in a similar way.
  4. This post uses Ruby on Rails, PostgreSQL, Sidekiq and Redis as services.

Requirements Unique to Local Development

  1. Debug: You might want to use pry or pry-byebug here.
  2. You want to mount the source code from your host machine and if you make changes on the host machine, you want to see the changes on the service on Docker containers seamlessly.
  3. You want to have a shell access to your local containers.
  4. You want to access to your application with http://localhost:3000

Basic Setup

This basic setup is taken from https://docs.docker.com/compose/rails/, in which case the base OS seems to be Ubuntu. I won’t include database.yml here so check it on the link!

Dockerfile

Change directory paths appropriately.

FROM ruby:2.5
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

docker-compose.yml

Change directory paths appropriately here also.

version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: postgres
      POSTGRES_DB: myapp_development
  redis:
    image: registry.of.somewhere/redis
  app:
    build:
      context: .
      args:
        BUILD_ENV: dev 
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db
      - redis
    env_file:
      - .env
    command:
      bundle install && rm -f tmp/pids/server.pid && bundle exec rails s -b 0.0.0.0 -p 3000
    stdin_open: true
    tty: true
  sidekiq:
    image: myapp:latest
    volumes:
      - .:/myapp
    depends_on:
      - db
      - redis
    env_file:
      - .env
    command:
      bundle install && bundle exec sidekiq'

Maybe you have noticed that some unfamiliar things have been added here, such as BUILD_ENV or automatically bundle install, removing pid file, enabling tty and stdio from app container.

BUILD_ENV is a way to specify a env the current Docker build is built for. You specify the default value in Dockerfile itself, but you can override the value in docker-compose.yml for local environment as above. The next section explains about it.

Also sometimes container stops accidentaly before it deletes the Rails pid file. After that happens, Rails won’t start because there is an old pid file remaining from the previous container start.

tty and stdin_open options are necessary if you want to debug with pry.

What if you want something special only for local on Dockerfile?

ARG is useful when you want to do something special for your local development only, and that’s what you saw in docker-compose.yml above. So add something like the following to the Dockerfile above.

ARG BUILD_ENV=production
...
RUN if [ "$BUILD_ENV" = "dev" ]; then \
      apt-get install -y \
        git \
        openssl \
        # For pry paging
        less; \
      mkdir -p /myapp/tmp; \
    fi

The docker-compose.yml above takes care of BUILD_ENV automatically but when you build image for development manually, specify BUILD_ENV=dev and development necessities will be installed.

Discussions

1. Development DB vs Test DB

Although you should have one service per container based on Docker philosophy, you might feel too much having 2 databases (dev and test) running for one application on your local machine. In fact, we put them together, but that means you have to do rake db:setup RAILS_ENV=test when necessary.

2. Speed up your local

You might find your local Docker application is sluggish when you access to it on your blowser. First try the Docker mount option delegated in docker-compose.yml as documented here: https://docs.docker.com/compose/compose-file/#caching-options-for-volume-mounts-docker-for-mac If you think that’s not enought, docker-sync is a very powerful tool so you can have native file access speed. But sometimes it causes troubles which was the case for me, too. So there is a tradeoff. If I have a chance, I will write another post about docker-sync.

3. Downside of putting the production/local Docker together

I had those two files (Dockerfile, Dockerfile.local) separated at the beginning since there were significant differences due to our own unique requirements related to Terraform, but thanks to some requirement change I put them together with the help of ARG at one point. But after that, every time I made changes on Dockerfile for local development I had to worry about the impact on production Docker image. So in my opinion, it still makes sense to have a separate Dockerfile.local, if you make a lot of changes and experiments for your development. And hey, even if you make duplication of codes which you and I don’t like, it’s always just two of them, guaranteed (or at least constant number).

Useful Bash Commands for Local Docker

Now I think you have all the necessary information for local docker setup. Congratulations!

Lastly these are the bash commands (mainly docker-compose commands) I use for daily local development. Hope this is useful to you, too.

# Start your containers
$ docker-compose up
# Start your containers and daemonize the processes
$ docker-compose up -d
# See the logs for app container
$ docker-compose logs app -f
# Stop containers. You can specify container name(s) too
$ docker-compose stop 
# In addition to stop, this will cleanup all containers states
$ docker-compose down 
# When you want a shell access, run the following. Change `sh` to `bash` if available
$ docker-compose exec app /bin/sh 
# When you binding.pry, this is how you interact with the ruby process. Exit with Ctrl-p + Ctrl-q.
$ docker attach my_app_1 

Also, you can always use docker commands, of course, whenever nencessary.

Happy local development with Docker!

[Edit] Added a post with a real example here.