Demystifying Docker Compose

Jenn - Nov 21 '19 - - Dev Community

My latest project used Rails and Postgres. After fighting my Mac and the Postgres install for an hour, I gave up and used Docker instead.

Docker has always seemed kinda mystical to me and I really wanted to understand my setup this time instead of just copying a quick start. And since there is no better way to learn something than to teach it, I will share it with you!

There are three key files:

  • Dockerfile
  • docker-compose.yml
  • database.yml

And two main commands:

  • docker-compose build
  • docker-compose up

You will need to have Docker installed.

I prefer to use the Docker extension for VSCode to interact with my running containers, so there are more commands you may need if you are not using that extension.

Dockerfile

The Dockerfile is where the dependencies for your container are defined. I'll go through mine line by line.

FROM ruby:2.6.3
This is from what image it is starting from, in this case the Ruby 2.6.3 image.

RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
RUN gem install bundler

Docker containers are VERY bare bones and the next lines were installing Nodejs, a Postgres client, and Bundler.

WORKDIR /my-api
This should be the folder where all your application code is in. It is setting the working directory so the next commands are easier.

COPY Gemfile /my-api/Gemfile
COPY Gemfile.lock /my-api/Gemfile.lock

Copying the Gemfiles to the working space in the container.

RUN bundle install
This will run bundler in the container.

COPY . /my-api
Copy all the other files into the working space.

COPY Docker/database.yml config/database.yml
This line can be skipped. The project was shared with others who had local installations of Postgres and I didn't want to override the config/database.yml in the repository. I created a Docker folder for my specific database.yml file. This command copies my database.yml file to the standard location for the container.

EXPOSE 3000
Only open the ports you need to have open.

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]

Start rails.

So for this Rails and Postgres project, my full Dockerfile looked like this:

FROM ruby:2.6.3
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client 
RUN gem install bundler
WORKDIR /my-api
COPY Gemfile /my-api/Gemfile
COPY Gemfile.lock /my-api/Gemfile.lock
RUN bundle install
COPY . /my-api
COPY Docker/database.yml config/database.yml 
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]

docker-compose.yml

This file connects our newly defined Rails container to a Postgres database.

version: '3'
All docker-compose files start with this. Don't change the version, just leave it.

services:
This defines the services (aka containers) to be built and how they are connect.

db:
image: postgres

The first service is a Postgres database. This is pulling the base Postgres image. I do not need any add-ons or customizations for my database so a Dockerfile for this service isn't necessary.
If you want data to persist, you will have to add a volume.
volumes:
- ./tmp/db:/var/lib/postgresql/data

I didn't need persistent storage for this project so I didn't use a volume.

web:
The next service is the Rails web server.

`build: .`

Build the image from our newly defined Dockerfile.

command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
Run this bash command in our new service container. This command is removing the pid file and telling rails what ports to use.

`ports:
  - "3000:3000"`

Mapping the Rails service port 3000 to my local machine's 3000.

`depends_on:
  - db`

This service will not start until the database service has started.

Once again, a volumes line could be here but I didn't need it.

All together my docker-compose.yml looks like this.

version: '3'
services:
  db:
    image: postgres
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    ports:
      - "3000:3000"
    depends_on:
      - db

database.yml

This is the database connection file.

default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  username: postgres
  password:
  pool: 5

development:
  <<: *default
  database: my-api_development


test:
  <<: *default
  database: my-api_test

docker-compose build

This command builds the containers. With my set up, anytime I update the Gemfile I have to rebuild.

Building can take some time. Each line in the docker-compose.yml is a step in the building process that can be cached so each build afterwards is faster.

Example output from a quick rebuild.

$ docker-compose build
db uses an image, skipping
Building web
Step 1/11 : FROM ruby:2.6.3
 ---> d529acb9f124
Step 2/11 : RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
 ---> Using cache
 ---> 32e807d05964
Step 3/11 : RUN gem install bundler
 ---> Using cache
 ---> 0c1c757c6905
Step 4/11 : WORKDIR /my-api
 ---> Using cache
 ---> b305131f9c6b
Step 5/11 : COPY Gemfile /my-api/Gemfile
 ---> Using cache
 ---> 657fc337d0c4
Step 6/11 : COPY Gemfile.lock /my-api/Gemfile.lock
 ---> Using cache
 ---> 09154df316d8
Step 7/11 : RUN bundle install
 ---> Using cache
 ---> 7915826ca085
Step 8/11 : COPY . /my-api
 ---> 4ff482944c8e
Step 9/11 : COPY Docker/database.yml config/database.yml
 ---> 101f814ba0bc
Step 10/11 : EXPOSE 3000
 ---> Running in 45f91676a8f3
Removing intermediate container 45f91676a8f3
 ---> 9426b9097b78
Step 11/11 : CMD ["rails", "server", "-b", "0.0.0.0"]
 ---> Running in 3f262f9679a2
Removing intermediate container 3f262f9679a2
 ---> a056b93d4bc8
Successfully built a056b93d4bc8

docker-compose up

This command starts the containers.

$ docker-compose up
Starting my-api_db_1 ... done
Recreating my-api_web_1 ... done
Attaching to my-api_db_1, my-api_web_1

Then visit localhost:3000 in a browser to confirm it is up and running.

For more information, check out the official quickstart guide for Rails.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . .