Red Green Repeat Adventures of a Spec Driven Junkie

Intro to Docker Compose

I enjoy using Docker for individual applications, but typing out each application’s commands (i.e. docker run <option_1> <option_2> image command) becomes tedious and error prone, especially across weekends. :-)

Also, each application has its own set of commands, that are in general similar, but have small idiosyncracies. For example: the commands to start a Ruby on Rails application and Angular application in development mode:

  • ng serve --host 0.0.0.0
  • rails server -b 0.0.0.0

Do you see the difference? Ruby on Rails uses server and -b for its host address while Angular uses serve and --host for similar settings. Such idiosyncracies become an annoyance over time.

At the same time, I am only working with two services, but with five or six applications, managing them would take exponentially more work.

Solution: Enter Docker Compose

The Docker Compose utility solves these problems if your individual applications can run in a Docker container.

I will take two docker containers I have worked on recently: Rails’ number_generator_app and Angular number-generator-service and write a docker compose configuration file to bring both services up ready for development with a single command.

Requirements

The main requirement for this article is:

  • Install Docker

If you would like to follow along, everything for this article is also available here

What is Docker Compose?

From Docker’s documentation website, Docker Compose is:

Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration. To learn more about all the features of Compose, see the list of features.

In my own words:

Docker Compose is a Docker container orchestration utility. Alowing users to seamlessly manage multiple containers.

To gain the full benefits of Docker Compose:

  • there is more than one application - like a microservice
  • each application can run in a Docker container

If either one of these are not true in your situation, Docker Compose will seem like extra work.

Configuration

Docker Compose is an application, but it requires a user generated configuration file. The main use for the configuration file is:

  • specify each application location
  • starting command for the application
  • port assignments
  • volumes

A configuration file to setup the Ruby on Rails number_generator_app server with the number-generator-service Angular application would be:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
version: '3.2'
services:
  rails:
    build: number_generator_app/
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    ports:
      - "3100:3000"
    volumes:
      - type: bind
        source: ./number_generator_app/
        target: /rails_app

  angular:
    build: number-generator-service/
    command: ng serve --host 0.0.0.0
    ports:
      - "4300:4200"
    volumes:
      - type: bind
        source: ./number-generator-service/
        target: /angular

I will go over each key section:

Services

The services entry lists all the top level application. In our case, rails and angular are the top level applcations. These are just labels and can easily modified to: backend and frontend respectively.

Build

The build option specifies the directory where to find the application, relative to the docker-compose.yml file.

Docker compose expects the application folder to have a Dockerfile that specifies how to build the Docker image to run the application.

Command

The command specifies how to start the application within the Docker container.

This command can be in the Dockerfile directly as CMD <command with options> but I prefer to have the command in the docker-compose configuration file.

Having the command in the configuration file allows the Docker image to be more flexible (i.e. using another pre-built Docker image directly.)

Ports

Specify the exposed ports in this option. As we have a frontend application connecting to a backend application through the host, these ports must match the ports specified within the application.

Volumes

This section details the volume sharing option between the host and the container. If you would like to load the application code from the host machine dynamically (i.e. local host changes reflect directly within the running container) include options to mount volumes into the container.

If the Docker image contains all of your application code, this configuration is optional.

_note: - only appears on the type option, otherwise errors such as: “‘type’ is a required option”_starts to appear when running docker-compose up

For example, if we write the voluems section as:

    volumes:
      - type: bind
      - source: ./number_generator_app/
      - target: /rails_app

Then this error will appear:

$ docker-compose up
ERROR: The Compose file './docker-compose.yml' is invalid because:
services.rails.volumes 'type' is a required property
services.rails.volumes 'type' is a required property

Building

With the docker-compose configuration file complete and applications are in place, we have to build any associated Docker images.

The easiest way to do that is use command: docker-compose build

This will build all the docker images in the configuration file.

bash-3.2$ docker-compose build
Building rails
Step 1/6 : FROM ruby:2.5.0-slim
 ---> 3d55149ba5af
...
Removing intermediate container 2ffb03977cbc
 ---> e5e89376481c
Successfully built e5e89376481c
Successfully tagged compose_rails:latest
Building angular
....
up to date in 8.83s
Removing intermediate container 83996d19e713
 ---> f8ee324680c1
Successfully built f8ee324680c1
Successfully tagged compose_angular:latest

Rebuilding

If there are any changes to the Dockerfile for applications listed in the Docker compose configuration file, use docker-compose build again to rebuild everything. Docker compose is smart enough to only build images with changes.

Running

With all Docker images listed in the docker compose configuration file built, it’s time to bring up the application. To start up every application, run:

bash-3.2$ docker-compose start
Starting compose_rails_1   ... done
Starting compose_angular_1 ... done

Yes, it is this simple. This is better than manually starting each one, which can become tedious and error-prone.

Stopping

To stop the application(s), you can type: ^C if you still have the console open or run:

$ docker-compose stop
Stopping compose_angular_1 ... done
Stopping compose_rails_1   ... done

Encountered Errors

After working with this new setup, I started to get this error:

$ docker-compose start
Starting compose_rails_1     ... done
Recreating compose_angular_1 ... done
Attaching to compose_rails_1, compose_angular_1
rails_1    | A server is already running. Check /rails_app/tmp/pids/server.pid.
rails_1    | => Booting Puma
rails_1    | => Rails 5.1.5 application starting in development
rails_1    | => Run `rails server -h` for more startup options
rails_1    | Exiting
compose_rails_1 exited with code 1
angular_1  | ** NG Live Development Server is listening on 0.0.0.0:4200, open your browser on http://localhost:4200/ **
...

This is peculiar as I did not run the Rails server. Shutting down the services with docker-compose stop showed the server was not running… So I was curious.

The solution I found was to remove the /rails_app/tmp/pids/server.pid file (listed in the error message!) and I was able to start the server again normally.

Conclusion

Instead of starting and stopping individual applications’ by hand, Docker Compose provides a simple one command method for this.

Its configuration file centralizes all of applications’ individual settings so it can be easily managed across restarts or even systems.