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 . /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.
- Make sure your containers has started already.
- Type
docker ps
to 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> + 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!