An oft-repeated and sensible principle in software engineering is DRY, or “don’t repeat yourself”. Here we will apply this principle to Docker compose files.

This post is part of the “Spring Boot Primer” series.

Intended audience

This post assumes a basic level of familiarity with Docker compose files. If you are not already familiar, there is good documentation available.

Dependencies

To build the source, you will need JDK 8+, and a Docker installation.

What are we aiming for?

Following the DRY principle, we would like to use Docker compose for both development and production environments, with as little duplication as possible.

Docker compose will support this with its ability to compose, or layer, multiple compose files together. In the application we are building, which is a simple Spring Boot based REST API, we have the following service dependencies:

  • Database (MariaDB)
  • Cache (Redis)
  • Administration console (Adminer)

Development

In development we want to run the Spring Boot app normally, and have it be able to connect to the database and cache inside the Docker network.

Production

In production we want to run the entire stack inside Docker, we don’t want to expose the database or cache, but we do want to expose the Spring Boot App.

Composing them together

This logically leads us to a set of three compose files, which will be pulled together in two different combinations, as below:

Common services

The database, cache and admin tools will be common between both production and development, so we create a shared docker-compose.yml as follows:

docker-compose.ymldocker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: '3.6'

services:
  db:
    image: mariadb
    environment:
      MYSQL_DATABASE: 'backend'
      MYSQL_USER: 'backend'
    command:
      - '--character-set-server=utf8mb4'
      - '--collation-server=utf8mb4_unicode_ci'

  adminer:
    image: adminer
    ports:
      - '8081:8080'
    environment:
      ADMINER_DEFAULT_SERVER: db

  cache:
    image: redis:alpine
    command: ["--notify-keyspace-events", "Egx"]

The object at line 4 defines our MariaDB service. We are asking for a database and user to be created (lines 7 and 8), and asking for UTF8 support to be enabled by default (lines 10 and 11).

Development

The development file needs to expose the ports for the database and cache so that a locally run version of the application can access them. It also wires a development-specific volume to the database service.

docker-compose-development.ymldocker-compose-development.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: '3.6'

volumes:
  db-data-development:
    driver: local

services:
  db:
    volumes:
      - db-data-development:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: 'Uv6DFjqyBbGxGczOaQFCP8FnmOwP98FxNqxRezUZ5'
      MYSQL_PASSWORD: 'XfeCEtSOFL91QpeyDxQnkRattHWzufTdDB1Pn5iB4'
    ports:
      - '3306:3306'

  cache:
    ports:
      - '6379:6379'

Running

To run up the development environment:

docker stack deploy --prune -c docker-compose.yml -c docker-compose-development.yml spring-rest-example

Then start the Spring Boot application in your IDE, or with the command:

./mvnw spring-boot:run

Production

The production file needs to start the Spring Boot application as a service, and expose it outside of the Docker network. It also needs to connect a volume to the database, that won’t get mixed up with the development version.

We would also like to use different passwords for development and production.

docker-compose-production.ymldocker-compose-production.yml
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
version: '3.6'

volumes:
  backend-data:
    driver: local
  db-data-production:
    driver: local

services:
  db:
    volumes:
      - db-data-production:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"
      MYSQL_PASSWORD: "${MYSQL_PASSWORD}"

  backend:
    image: petewoods/spring-rest-example:latest
    deploy:
      replicas: 2
    volumes:
      - backend-data:/var/lib/data
    environment:
      DB_VENDOR: 'mariadb'
      DB_ADDR: 'db'
      DB_NAME: 'backend'
      DB_USER: 'backend'
      DB_PASSWORD: "${MYSQL_PASSWORD}"
      DB_DRIVER: 'org.mariadb.jdbc.Driver'
      SESSION_HOST: 'cache'
      SESSION_PASSWORD:
      SESSION_PORT: 6379
      MEDIA_LOCATION: 'file:/var/lib/data/'
      GOOGLE_CLIENT_ID: "${GOOGLE_CLIENT_ID}"
      GOOGLE_CLIENT_SECRET: "${GOOGLE_CLIENT_SECRET}"
      # AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}"
      # AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}"
      # CLOUDWATCH_METRICS_ENABLED: 'true'
      # CLOUDWATCH_METRICS_NAMESPACE: 'production-spring-rest-example'
      # MEDIA_LOCATION: 's3://my-bucket'
    ports:
      - '8080:8080'

Running

To run up the production environment, we first need to build the image:

docker build -t petewoods/spring-rest-example .

Then we can run docker stack deploy:

(. production.env && docker stack deploy --prune -c docker-compose.yml -c docker-compose-production.yml spring-rest-example)