Using Docker with Microservices

Now that we know how Java works inside a container, we can start using Docker with our microservices. Before running a microservice as a Docker container, we first need to create a Docker image. To do this, we write a file called a Dockerfile, which tells Docker how to build the image.

Since a microservice running in a container is isolated from others—it has its own IP address, hostname, and ports—it needs a different configuration than when it runs on the same computer as other microservices.

For example, because the microservices are no longer on the same host, we don’t have to worry about port conflicts. We can use the default port 8080 for all microservices. However, if one microservice needs to communicate with another, we cannot use localhost anymore, because each container has its own network address.

To manage these differences, we use Spring profiles, which let us define configuration for different environments. We will create a new profile called docker for running microservices inside Docker containers.

Add this at the end of the application.yml file:

server:
  port: 8082
spring:
  application:
    name: course-service
logging:
  level:
    root: INFO
    com.htp: DEBUG
---
spring.config.activate.on-profile: docker
server.port: 8080

Tip: Spring profiles help you use different configurations for development, testing, production, or Docker. Values in a profile replace the default settings. Using YAML, you can put multiple profiles in the same file, separated by ---.

Here is a simple Dockerfile for our microservice:

FROM openjdk:17
EXPOSE 8080
ADD ./build/libs/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

Some important points:

Problems with This Simple Approach

A better way is to split the image into layers:

This way, Docker can reuse layers that haven’t changed, making builds faster.

Using a Smaller Java Image

The OpenJDK project doesn’t provide a small Java 17 JRE Docker image. But Eclipse Temurin does. Temurin offers both full JDK and smaller JRE images.

Handling Fat JAR Files Efficiently

Spring Boot 2.3.0+ allows us to extract fat JARs into folders, which makes Docker images smaller and faster. By default, Spring Boot creates:

We can create one Docker layer per folder. This improves caching and speeds up image builds.

Multi-Stage Dockerfile Example:

FROM eclipse-temurin:17.0.5_8-jre-focal as builder
WORKDIR extracted
ADD ./build/libs/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

FROM eclipse-temurin:17.0.5_8-jre-focal
WORKDIR application
COPY --from=builder extracted/dependencies/ ./
COPY --from=builder extracted/spring-boot-loader/ ./
COPY --from=builder extracted/snapshot-dependencies/ ./
COPY --from=builder extracted/application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

Here’s how it works:

  1. Builder stage
    • Uses Temurin JRE 17 image.
    • Extracts the fat JAR into the extracted folder.
  2. Final stage
    • Uses the same Temurin image.
    • Copies the extracted folders from the builder stage.
    • Each folder becomes a Docker layer.
    • Exposes port 8080.
    • Starts the microservice using JarLauncher.

Using multi-stage builds, we keep the final image small while handling all packaging steps.

Building a Docker Image

Before we can run our microservice in Docker, we need to build the deployment artifact—the fat JAR file—for course-service

pwd
/microservices-with-spring-boot

./gradlew :microservices:course-service:build

ls -l microservices/course-service/build/libs/
-rw-r--r-- 1 htp 1049089 26907426 Nov 26 16:02 course-service-1.0.0-SNAPSHOT.jar

Here:

Next, we create a Docker image and give it the name course-service:

cd microservices/course-service/

ls
Dockerfile  build/  build.gradle  settings.gradle  src/

docker build -t course-service .

To check the image exists:

docker images | grep course-service

course-service   latest   89df1ba914e4   57 seconds ago   434MB

We can now start the microservice:

docker run --rm -p8080:8080 -e "SPRING_PROFILES_ACTIVE=docker" course-service

Here’s what this command does:

We can now access the microservice from another Terminal:

curl localhost:8080/api/v1/courses/1

Expected output

{
  "courseId": 1,
  "title": "Introduction to Spring Boot",
  "description": "Learn the basics of Spring Boot 3.",
  "serviceAddress": "******:8080",
  "difficultyLevel": "MEDIUM"
}

To see all running containers:

docker ps
CONTAINER ID   IMAGE            COMMAND                  CREATED         STATUS         PORTS                                         NAMES
bdd38c424630   course-service   "java org.springfram…"   6 minutes ago   Up 6 minutes   0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp   elated_sutherland

To stop the container, press Ctrl + C in the Terminal.

Detached Mode

So far, when we start a Docker container using docker run, the Terminal is locked because the container is running in the foreground. This can be inconvenient if we want to keep using the Terminal for other commands.

Docker allows us to run containers in detached mode, meaning the container runs in the background, and the Terminal stays free.

To start a container detached, use the -d option. You can also give it a name with --name (optional, but helpful):

docker run -d -p8080:8080 -e "SPRING_PROFILES_ACTIVE=docker" --name CS course-service

Use docker ps to see all running containers:

docker ps

CONTAINER ID   IMAGE            COMMAND                  CREATED          STATUS          PORTS                                         NAMES
9117f2b93d05   course-service   "java org.springfram…"   49 seconds ago   Up 49 seconds   0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp   CS

Even in detached mode, you can still see the container logs with:

docker logs CS -f

Try running a request to your microservice while viewing logs:

curl localhost:8080/api/v1/courses/1

When you’re done, stop and remove the container:

docker rm -f CS

The source code for this article is available over on GitHub.