On Beer Composition – Orchestrating our Containers with Docker Compose

Just like composing a good craft beer requires a good mix of complementing types of hops, malts, fruits, spices and herbs – and of course the craftsmanship of a good brewer – getting the right Docker composition is a craft on its own.

Or to use a different analogy: composing your Docker containers in a simple and declarative manner is just like stacking beer cans and bottles in containers in the most optimal way, with the least amount of manual labor. Both highly increase the revenue of the craft beer business, which let the people managing Terrax Microbrewery Inc. focus on counting money, instead of worrying about tedious matters like getting the sequence right for loading beers into containers.

This post is all about container composition and before Terrax Microbrewery Inc. takes automation to the next level by diving into the slightly more complex and interesting world of Kubernetes, let’s do a quick blog and improve some more upon the setup of our previous blogpost.

The code for this blogpost can as always be found on GitHub here.

Recap

We ended our previous blogpost with a composition that could be created from scratch using the following set of Docker commands:

1
2
3
4
5
6
docker volume create tb-mysql-shared
docker network create tb-network-shared
docker run --name tb-mysql-db --detach --network tb-network-shared --mount source=tb-mysql-shared,target=/var/lib/mysql rphgoossens/tb-mysql-docker:tb-docker-2.0
docker run --name tb-springboot-app-1 --detach --network tb-network-shared -e "SERVER_PORT=8090" -e "DB_USERNAME=tb_admin" -e "DB_PASSWORD=tb_admin" -e "DB_URL=mysql://tb-mysql-db:3306/db_terrax" rphgoossens/tb-springboot-docker:tb-docker-2.0
docker run --name tb-springboot-app-2 --detach --network tb-network-shared -e "SERVER_PORT=8090" -e "DB_USERNAME=tb_admin" -e "DB_PASSWORD=tb_admin" -e "DB_URL=mysql://tb-mysql-db:3306/db_terrax" rphgoossens/tb-springboot-docker:tb-docker-2.0
docker run --detach --network tb-network-shared --publish 8080:8080 rphgoossens/tb-nginx-docker:tb-docker-2.0

After successful execution we end up with a load balanced Spring Boot REST service that enables us to retrieve and store important Beer data in a mysql db. Now, running all these commands in the right sequence is a tedious and error prone job. Luckily there is a better way of doing a so called multi-container orchestration: enter Docker Compose!

Docker Compose

Instead of executing multiple Docker commands to fire up a volume, a network and multiple containers, Docker Compose uses a declarative YAML file to setup the entire orchestration. Firing up the multi container constellation becomes a one liner using Docker Compose. Once you get the YAML file right, that is!

YAML file

To learn more about Docker Compose and the contents of the YAML file, you can check the Docker Compose website https://docs.docker.com/compose.

To keep this post brief we’ll show you guys the YAML file that’s going to replace all the manual Docker commands shown in the previous section. And just like brewing a good beer takes a few attempts, getting to the first working docker-compose.yml file took a few tries as well…

First attempt

The fIrst attempt at a docker-compose file just used the images we created in the previous blogpost:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
version: "3.3"
services:
  tb-mysql-db:
    image: rphgoossens/tb-mysql-docker:tb-docker-2.0
    volumes:
      - type: volume
        source: tb-mysql-shared
        target: /var/lib/mysql
 tb-springboot-app-1:
    image: rphgoossens/tb-springboot-docker:tb-docker-2.0
    environment:
      - SERVER_PORT=8090
      - DB_USERNAME=tb_admin
      - DB_PASSWORD=tb_admin
    depends_on:
      - tb-mysql-db
tb-springboot-app-2:
    image: rphgoossens/tb-springboot-docker:tb-docker-2.0
    environment:
      - SERVER_PORT=8090
      - DB_USERNAME=tb_admin
      - DB_PASSWORD=tb_admin
    depends_on:
      - tb-mysql-db
  tb-nginx:
    image: rphgoossens/tb-nginx-docker:tb-docker-2.0
    ports:
      - "8080:8080"
    depends_on:
      - tb-springboot-app-1
      - tb-springboot-app-2
volumes:
  tb-mysql-shared:

When looking at the list of docker commands in the Recap section, this docker-compose file should make a lot of sense. We declared 4 services, one for every container that has to be spun up and a volume section for persisting the tb-mysql-db service data. All services are based on the images we built in the previous blog and the spring boot app services sections contain all the necessary environment variables that we previously added to the docker commands.

The only real addition here are the depends_on settings added to some services. This makes sure that containers start up only when the containers they depend upon have been started as well.

Also notice that there’s no network defined in the docker-compose file. It’s not really necessary. A default network (called tbappdocker_default in this case) is created when the compose file is run and all services will join it. We’ll improve a bit upon this first setup later on when we will also fix the biggest problem of this first attempt.

The docker-compose file is stored as app.yml (but you can choose any name you want). If you’ve installed docker-compose (https://docs.docker.com/compose/install/) on your machine you can fire up the composition right away (The -d is for running the constellation in detached mode):

1
docker-compose -f app.yml up -d

When you list the containers (docker ps) you’ll notice that a few seconds after startup the Spring Boot containers will go down again. If you wait a minute and run the docker compose file again, the containers will start and will continue to be in the RUNNING state.

What’s going on here? Examining the docker logs (docker logs tbappdocker_tb-springboot-app-1_1) of one of the crashed containers, should give you a clue:

org.springframework.beans.factory.BeanCreationException: Error creating bean 
with name 'entityManagerFactory' defined in class path resource
[org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]

The issue clearly has something to do with the database connection. The problem is that the Spring Boot containers are started when the mysql db is not entirely ready. It takes a while after the container is started for the mysql db to becomes fully available.

So how do we fix this? We need a way to postpone starting the spring boot containers until the database is fully operational. The solution is described somewhere in the Docker Compose manuals: https://docs.docker.com/compose/startup-order/.

The idea is to add a shell script to the Spring Boot containers. The shell script should be the first thing that runs on startup of the container and will wait for the mysql db to be fully operational. Then, and only then, will the spring boot app be started. No need to reinvent the wheel here: a wait-for-it.sh shell script can be found here: https://github.com/tunix/docker-compose-demo/blob/master/wait-for-it.sh.

To get the shell script into the Spring Boot containers we will need to bake a new image. Instead of pushing a new image to DockerHub and changing the references in the docker-compose file to reflect this newer image, let’s build the image as part of our docker-compose file.

Final version

So with the knowledge that we need to put in that wait-for-it.sh script and that we could also improve our network layout a bit, without further ado, here is the final version of our app.yml file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
version: "3.3"
services:
  tb-mysql-db:
    image: rphgoossens/tb-mysql-docker:tb-docker-2.0
    volumes:
      - type: volume
        source: tb-mysql-shared
        target: /var/lib/mysql
    networks:
      - tb-network-backend
  tb-springboot-app-1:
    build: ./tb-springboot-app
    image: rphgoossens/tb-springboot-docker:tb-docker-2.1
    environment:
      - SERVER_PORT=8090
      - DB_USERNAME=tb_admin
      - DB_PASSWORD=tb_admin
    depends_on:
      - tb-mysql-db
    command: ["/wait-for-it.sh", "tb-mysql-db:3306", "--timeout=0", "--", "java","-cp","app:app/lib/*","nl.terrax.tbspringbootdocker.TbSpringbootDockerApplication"]
    networks:
      - tb-network-frontend
      - tb-network-backend
  tb-springboot-app-2:
    image: rphgoossens/tb-springboot-docker:tb-docker-2.1
    environment:
      - SERVER_PORT=8090
      - DB_USERNAME=tb_admin
      - DB_PASSWORD=tb_admin
    depends_on:
      - tb-mysql-db
    command: ["/wait-for-it.sh", "tb-mysql-db:3306", "--timeout=0", "--", "java","-cp","app:app/lib/*","nl.terrax.tbspringbootdocker.TbSpringbootDockerApplication"]
    networks:
      - tb-network-frontend
      - tb-network-backend
  tb-nginx:
    image: rphgoossens/tb-nginx-docker:tb-docker-2.0
    ports:
      - "8080:8080"
    depends_on:
      - tb-springboot-app-1
      - tb-springboot-app-2
    networks:
      - tb-network-frontend
volumes:
  tb-mysql-shared:
networks:
  tb-network-frontend:
  tb-network-backend:

Notice that only the first Spring Boot app service actually builds the new image. The second one just references this new image (this will prevent the image from being built twice).

The command directives in the Spring Boot app service ensures that the wait-for-it.sh script is executed first and takes care that the Spring Boot app will be started only after the mysql database is available on port 3306.

We’ve also improved the container networking a bit. Instead of relying on one default network, we’ve separated the network into a frontend and a backend network. With this setup the mysql-db container won’t be available anymore from the nginx container.

Last piece of the puzzle is the Dockerfile we use for building the new Spring Boot app image:

1
2
3
4
5
FROM rphgoossens/tb-springboot-docker:tb-docker-2.0
COPY wait-for-it.sh /wait-for-it.sh
RUN chmod +x /wait-for-it.sh
RUN apk update && apk add bash
ENTRYPOINT []

This Dockerfile copies the wait-for-it.sh shell script into the Spring Boot image, adds the bash shell, and overrides the ENTRYPOINT instruction to prevent the spring boot app from starting before the wait-for-it.sh script is executed.

Fire up the containers based on this new docker-compose file (the –build option (re-)builds the Spring Boot app image before starting up the containers):

1
docker-compose -f app.yml up -d --build

Now this time you’ll notice that the spring boot app containers keep running:

You can check the HOSTNAME environment variable using the actuator (http://localhost:8080/actuator/env/HOSTNAME) and see that it switches between two values, proving that the nginx load balancer is doing its proper job.

You can also check the swagger ui again at http://localhost:8080/swagger-ui.html and play a little bit with the API.

If you need to check the inner contents of the new Spring Boot image you can use:

1
docker run -ti --entrypoint /bin/sh rphgoossens/tb-springboot-docker:tb-docker-2.1

Checking the spring boot app at runtime now that the image contains the bash shell can be done with:

1
docker exec -ti tbappdocker_tb-springboot-app-1_1 bash

When you’re done playing with the app, you can shut down the entire constellation with one Docker Compose command:

1
docker-compose -f app.yml down

This will shut down and remove all containers and networks. Only the volume created will be preserved.

To clean up the volume as well, you can use:

1
docker-compose -f app.yml down --volumes

Summary

When working with multiple related Docker containers using Docker Compose can greatly simplify starting all of them up in the right sequence. The setup of the constellation is put in one declarative yaml file and the Docker Compose CLI takes care of starting up the composition with one single command. Shutting down the whole composition only takes one single command as well. As an added benefit we can put those Docker Compose files in a version control system.

That’s it for now! In the next blogpost we will enter the brave new world of Kubernetes and try to get this app setup running in one of the many Kubernetes engines out there. So for now: grab a well deserved beer and stay tuned!

References