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.