Love, docker-compose!

#Ruby#Rails#Postgres#Docker#Docker-compose

The Part 2 of this series ended with two docker commands that start an app and a database container. Long and verbose, they aren’t practical to use regularly during the work. Worse: more commands are needed to be able to run database migrations, install new gem/yarn dependencies, debug code, and so on. After moving the complexity from the command line to the docker-compose.yml configuration file, this article demonstrates a possible Docker-on-Rails style workflow.

Composing the Postgres settings

Starting a container can be as easy as docker-compose --rm up pg, by moving all the command line settings to the docker-compose.yml file:

% docker run --rm                       \
  -v databases:/var/lib/postgresql/data \
  -e POSTGRES_HOST_AUTH_METHOD=trust    \
  --network demo-network                \
  --network-alias pg                    \
  -dp 5432:5432 postgres

is translated to the following settings. It’s not necessary to explicitly declare a network, because Docker Composer implicitly creates it, as well the name of the service is implicitly set to its network alias.

version: "3.8"

volumes:
  databases:

services:
  pg:
    image: postgres
    ports:
      - 5432:5432
    volumes:
      - databases:/var/lib/postgresql/data
    environment:
      POSTGRES_HOST_AUTH_METHOD: trust

Let’s test. The demo_default network is automatically created, as well as the demo_pg_1 container. The Postgres directory already exists because the databases volume was already previously mounted in this series’ Part 2.

% docker-compose up pg
Creating network "demo_default" with the default driver
Creating demo_pg_1 ... done
Attaching to demo_pg_1
pg_1  |
pg_1  | PostgreSQL Database directory appears to contain a database; Skipping initialization
pg_1  |
pg_1  | 2021-11-05 18:20:13.060 UTC [1] LOG:  starting PostgreSQL 14.0 (Debian 14.0-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
pg_1  | 2021-11-05 18:20:13.061 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
pg_1  | 2021-11-05 18:20:13.061 UTC [1] LOG:  listening on IPv6 address "::", port 5432
pg_1  | 2021-11-05 18:20:13.066 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"

Composing the App Settings

Let’s see how to configure this command-line into docker-compose.yml.

% docker run --rm             \
  --network demo-network      \
  -v "$(pwd):/src"            \
  -v "gems:/usr/local/bundle" \
  -p 3000:3000                \
  -e POSTGRES_HOST=pg         \
  -e POSTGRES_USER=postgres   \
  demo rails s -b 0.0.0.0

Again, the network can be omitted because of the composer-created demo_default network. The bind volume declaration is simpler .:/src because it doesn’t have to invoke the command line’s pwd utility. The gems named volume is declared, as well the ports mapping and the Postgres env vars.

The build block is configured in a way that if something changes on the Dockerfile a new image will be built the next time docker-compose initializes. Additionally, the depends_on declaration tells Docker to init the pg container before the app container.

volumes:
  databases:
  gems:

services:
  pg:
    ...
  app:
    build:
      context: .
      dockerfile: Dockerfile
    command: /bin/bash -c "bundle && rails db:create db:migrate && rm -f tmp/pids/server.pid && rails s -b 0.0.0.0"
    ports:
      - 3000:3000
    volumes:
      - ./:/src
      - gems:/usr/local/bundle
    environment:
      POSTGRES_HOST: pg
      POSTGRES_USER: postgres
    depends_on:
      - pg

Wrapping up

No more verbose commands to remember. And if using the Control + R history utility, starting the App and the Postgres containers is as simple as Control + R up and shutting down is Control + R down.

# History command: Control + R up
% docker-compose up -d
demo_pg_1 is up-to-date
Starting demo_app_1 ... done

# History command: Control + R down
% docker-compose down
Stopping demo_app_1 ... done
Stopping demo_pg_1  ... done
Removing demo_app_1 ... done
Removing demo_pg_1  ... done
Removing network demo_default

This is just an initial demonstration of how convenient and powerful Docker Compose can be to set up everything. Considering the app will evolve and need more services like Redis, Sidekiq, ElasticSearch, etc, and it’s easy to see how it can help on improving productivity and create standardized working environments.

Working on containers

Logs

# History command: Control + R logs
% docker-compose logs -f
% docker-compose logs -f app  # app logs only
% docker-compose logs -f pg   # postgres logs only

Rails commands

The --rm flag is useful to automatically remove the disposable command container from the Docker Desktop list.

% docker-compose run --rm app rails c
% docker-compose run --rm app rails db:reset
% docker-compose run --rm app rails db:migrate
% docker-compose run --rm app rails test

It’s smart to add an alias for docker-compose run --rm app into the .bashrc or .zshrc files:

alias drails="docker-compose run --rm app rails"

Funny, don’t you think?

% drails c
% drails db:reset
% drails db:migrate
% drails test

Debugging

In the container, an annoying message gets printed on the logs, and it tells us the web console won’t be available to debug exceptions in the development mode:

Cannot render console from 172.26.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1

It can be fixed by adding this piece of code to config/environments/development.rb:

Rails.application.configure do
  config.web_console.permissions = '172.0.0.0/8'
  ...

Finally, if a byebug instruction is added to a Ruby code, for example on the Home#index action, the app will not stop on the breakpoint although Docker prints the debugging info into the logs:

Debugging on Docker

In order to make it work, it’s necessary to add 2 settings to the app config:

    stdin_open: true
    tty: true

After restarting the containers and refreshing the page the breakpoint will work, and the last thing to do is attaching the current terminal to the app container stdin. Terminating the debugging session can be done using the sequence Control + P,Q (Control + D would terminate the container execution).

% docker container ls
CONTAINER ID   IMAGE      COMMAND                  CREATED         STATUS         PORTS                    NAMES
1c7b61d8c275   demo_app   "/bin/bash -c 'bundl…"   2 minutes ago   Up 2 minutes   0.0.0.0:3000->3000/tcp   demo_app_1
9974f03b8b06   postgres   "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes   0.0.0.0:5432->5432/tcp   demo_pg_1

% docker attach 1c7b61d8c275
(byebug)

Conclusion

I’m liking to learn Docker and I know what I learned and demonstrated on these 2 blog posts is just the beginning. I expect to post more articles while I keep learning this fascinating tool. The next articles will be focused on how I used this environment to develop a Hotwire application.

About Fábio Miranda

Photo of Fábio Miranda I'm currently living in Vancouver, Beautiful British Columbia, Canada, where my family and I started a new journey in 2023. I'm a hard-worker, currently the CTO at 7GEN, helping to remove the biggest barriers to electrification for medium- and heavy-duty commercial fleets.
Computer Engineer since 2004, graduated from ITA - Instituto Tecnológico de Aeronáutica (Aeronautical Technological Institute). I've been working with software development since then in many different tech stacks (Java, Python, Ruby, Golang, NodeJS) and industries (Aviation, Consulting, Brazilian Payments processing startups, Schools, Fundraising, and Logistics).