Dockerfiles per Service

One Image per Service

Every service is built into its own Docker image and versioned on its own: shopease/order-service:1.4.2, shopease/payment-service:2.0.1. The image contains the JRE and the app, so it runs the same on a laptop, in CI and in production.

Tip: Docker basics (images, containers, layers, volumes) are covered in the Careeroza Docker material, and a first Spring Boot Dockerfile in the Spring Boot advance level. Here we build images that are small, secure and fast to rebuild, for many services.

A Production Dockerfile (Multi-Stage, Layered)

# ---------- build stage ----------
FROM eclipse-temurin:21-jdk AS build
WORKDIR /workspace
COPY mvnw pom.xml ./
COPY .mvn .mvn
RUN ./mvnw -q dependency:go-offline          # cached until pom.xml changes
COPY src src
RUN ./mvnw -q package -DskipTests
# split the fat jar into layers: dependencies change rarely, application code often
RUN java -Djarmode=tools -jar target/order-service.jar extract --layers --launcher --destination extracted

# ---------- runtime stage ----------
FROM eclipse-temurin:21-jre
RUN useradd --system --uid 1001 spring
WORKDIR /app
COPY --from=build /workspace/extracted/dependencies/ ./
COPY --from=build /workspace/extracted/spring-boot-loader/ ./
COPY --from=build /workspace/extracted/snapshot-dependencies/ ./
COPY --from=build /workspace/extracted/application/ ./
# never run as root
USER spring
EXPOSE 8082
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
  • Multi-stage: Maven and the JDK stay in the build stage; the final image only has the JRE (~200 MB smaller).
  • Layers: after a code change only the small application layer (~100 KB) is rebuilt and pushed, not 80 MB of dependencies.
  • -Djarmode=tools is the Spring Boot 3.3+ way (older versions use -Djarmode=layertools). Set <finalName>order-service</finalName> in the pom so the jar name is fixed.
  • MaxRAMPercentage=75 sizes the heap from the container memory limit, leaving room for metaspace and threads.

Build and Run

Example

docker build -t shopease/order-service:1.4.2 ./order-service
docker images shopease/order-service
docker run -d --name order -p 8082:8082 -e SPRING_PROFILES_ACTIVE=docker shopease/order-service:1.4.2

Example output

[+] Building 41.3s (17/17) FINISHED
 => [build 6/7] RUN ./mvnw -q package -DskipTests                     31.2s
 => [stage-1 6/7] COPY --from=build /workspace/extracted/application/ ./  0.1s
 => naming to docker.io/shopease/order-service:1.4.2

REPOSITORY              TAG     IMAGE ID       CREATED          SIZE
shopease/order-service  1.4.2   3f9c1b2a7d4e   10 seconds ago   312MB

# rebuild after changing one Java file:  [+] Building 9.8s  (dependency layers CACHED)

Tip: No Dockerfile at all: ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=shopease/order-service:1.4.2 uses Cloud Native Buildpacks to create a layered, non-root image. Add a .dockerignore with target/, .git/ and *.log to keep the build context small.