Dark mode

{ Code } Faster!

Dockerizing Multiple Spring Boot Services in a Monolithic Repo

20 November, 2019

Last night I was writing multiple spring boot services for a programming challenge; I decided to complicate things a little bit and created these services as a monolithic repository and springboot nano-services where each service could only do one thing, because why not?

So in this post I’m going to show you how I built the docker images for all of the spring boot services and then used docker-compose to encapsulate the entire build and deployment process using a single file.

Example codebase

I will use a small example codebase to explain how to use Docker for a monolithic repository. For this example I built 3 services:

  • app
  • data-retriever
  • classifier

Structure is as follow:

.
├── README.md
├── app-service
│   └── src
│       ├── main
│       │   └── resources
│       └── test
│           └── resources
├── data-retriever-service
│   └── src
│       ├── main
│       │   └── resources
│       └── test
│           └── resources
└── classifier-service
    └── src
        ├── main
        │   └── resources
        └── test
            └── resources

1. Writing the Dockerfile

Firstly we will need to write a Dockerfile for each of the spring boot nano-services. they will all be very similar so let’s write the first Dockerfile for the app-service:

FROM gradle:jdk8-alpine as builder
COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle build

FROM openjdk:8-jdk-alpine
COPY --from=builder /home/gradle/src/build/libs/app-service-0.0.1-SNAPSHOT.jar /app/app-service.jar
WORKDIR /app
EXPOSE 8091
CMD ["java","-XX:+UseG1GC", "-jar","app-service.jar"]

I’ll explain line by line this is a multi-stage Dockerfile that does two things:

  • builds and packs the fat jar
  • runs the jar

First Stage - The build (line by line)

FROM gradle:jdk8 as builder

The keyword FROM tells Docker to use a given base image as a build base. We have used ‘gradle’ with tag ‘jdk8-alpine’. Think of a tag as a version. The image is labelled as builder and it will be is used to run gradlew and build the fat jar,

COPY --chown=gradle:gradle . /home/gradle/src

COPY instruction tells Docker to copy files from the local file-system to a specific folder inside the build image.

WORKDIR /home/gradle/src

WORKDIR instruction sets the working directory

RUN gradle build

RUN instruction tells Docker to execute a shell command-line within the target system. Here we executed a command to run gradle and build us the .jar file

Second Stage - The run (line by line)

FROM openjdk:8-jdk-alpine

We select a new image. We have used ‘openjdk’ with tag ‘8-jdk-alpine’.

COPY --from=builder /home/gradle/src/build/libs/app-service-0.0.1-SNAPSHOT.jar /app/app-service.jar

We copy the jar previously built as part of the “build image” to the new “run image”.

WORKDIR /app

sets the working directory

EXPOSE 8090

The EXPOSE instruction informs Docker that the container listens on the specified network ports at runtime. Here we set our port to 8090

CMD ["java","-XX:+UseG1GC", "-jar","app-service.jar"]

CMD instruction sets the command to be executed when running the image. Here we will run the fat .jar using G1GC garbage collector:

Note: There can only be one CMD instruction in a Dockerfile. If you list more than one CMD then only the last CMD will take effect.

Voila!

Now that we have our first Dockerfile we will need to create 2 more (1 for each of the remaining services) To do this just copy and paste the same docker file and replace app-service with the target service (data-retriever or classifier-service) change the port to your desired port and place it on the root of each service.

You’ll should end up with the following structure:

.
├── README.md
├── app-service
│   ├── Dockerfile
│   └── src
├── data-retriever-service
│   ├── Dockerfile
│   └── src
└── classifier-service
    ├── Dockerfile
    └── src

2. Writing the docker-compose.yml

We are going to use docker-compose to encapsulate the entire build and deployment process in a single file. This allows us to build and deploy the images with a single invocation of docker-compose up.

version: "3"

services:
  app-service:
    build:
      context: app-service
      dockerfile: ./Dockerfile
    ports:
      - 8090:8080
    networks:
      - challenge-network
    depends_on:
      - data-retriever-service
      - classifier-service
  data-retriever-service:
    build:
      context: data-retriever-service
      dockerfile: ./Dockerfile
    ports:
      - 8091:8080
    networks:
      - challenge-network
  classifier-service:
    build:
      context: classifier-service
      dockerfile: ./Dockerfile
    ports:
      - 8092:8080
    networks:
      - challenge-network
networks:
  challenge-network:
    driver: bridge

This docker-compose.yml defines the services that we need to run together in an isolated environment. Also creates a network called challenge-network and we will run our containers in the network so they can immediately communicate with other containers in the network. We will place the docker-compose.yml at the root of our project. so your final structure will looks like this:

.
├── README.md
├── docker-compose.yml
├── app-service
│   ├── Dockerfile
│   └── src
├── data-retriever-service
│   ├── Dockerfile
│   └── src
└── classifier-service
    ├── Dockerfile
    └── src

3. Running it

Now we can simply run docker-compose up it will create our 3 docker containers and start them up. and they will be available on the ports that we defined in our .yml previously.