Project 2 of 12 · project brief
Dockerize the Spring Boot Service
A small, secure, cache-friendly image for the API, pushed to a registry with proper tags.
The scenario
Project 1 works, but 'it works on that VM' doesn't scale. The team wants the API packaged as a container image that runs identically on a laptop, in CI, and in production. Your brief: write a production-grade Dockerfile, keep the image small and non-root, and publish versioned images to a registry.
Before you start
- Docker course
Dockerfiles, layers, multi-stage builds, registries.
- Docker · JVM in containers
Heap sizing and container-aware JVMs.
Stack
Target architecture
Deliverables and requirements
You will hand in
- A multi-stage
Dockerfileand.dockerignore - An image under ~250 MB running as a non-root user
- Images pushed with a semantic version tag and a Git SHA tag
- A Trivy scan report with no critical vulnerabilities
Functional
docker run -p 8080:8080 -e SPRING_DATASOURCE_URL=... shoplite-api:<tag>starts the API- Configuration only via environment variables (12-factor)
- A
HEALTHCHECKor health endpoint usable by orchestrators
Non-functional
- Rebuild after a code-only change takes seconds (dependency layer cached)
- Non-root user, no build tools or source in the final image
- JVM respects container memory limits (
-XX:MaxRAMPercentage)
Milestones
- 1
A naive image, measured
Done when: A working single-stage image, and its size and build time as a baseline.
- Write a Dockerfile
FROM maven:3.9-eclipse-temurin-21that builds and runs the JAR - Record image size (
docker images) and rebuild time after changing one line of code
Prove it works
terminal$ docker images shoplite-api:naive --format '{{.Size}}'── expected output ──812MB - Write a Dockerfile
- 2
Multi-stage with layer caching
Done when: A build stage with cached dependencies and a small JRE runtime stage.
- Stage 1: copy
pom.xmlfirst, runmvn dependency:go-offline, then copysrcand package - Stage 2:
java -Djarmode=tools -jar app.jar extract --layersto split dependencies from app classes - Stage 3:
eclipse-temurin:21-jre, copy layers in order of change frequency - Add
.dockerignore(target/, .git, IDE files)
Stuck? Hints
- Order COPY instructions from least to most frequently changing so code edits don't invalidate the dependency layer.
docker build --progress=plainshows which steps are CACHED.
Prove it works
terminal$ docker build -t shoplite-api:dev . && docker images shoplite-api:dev --format '{{.Size}}'── expected output ──241MB - Stage 1: copy
- 3
Harden it
Done when: Non-root, minimal, container-aware runtime.
- Create and switch to a non-root user (
USER 10001) - Set
JAVA_TOOL_OPTIONS=-XX:MaxRAMPercentage=75 - Add a
HEALTHCHECK(or rely on orchestrator probes) andEXPOSE 8080 - Scan with
trivy imageand fix critical findings (update base image)
Prove it works
terminal$ docker run --rm shoplite-api:dev id; trivy image --severity CRITICAL --exit-code 1 shoplite-api:dev && echo clean── expected output ──uid=10001 gid=10001 groups=10001clean - Create and switch to a non-root user (
- 4
Tag and push
Done when: Versioned, traceable images in a registry.
- Tag with the app version and
sha-<short git sha>; never rely onlatest - Push to GHCR, Docker Hub, or ECR
- Add OCI labels (
org.opencontainers.image.revision,source)
Prove it works
terminal$ docker buildx imagetools inspect ghcr.io/<you>/shoplite-api:sha-3f2a1c9 | head -3── expected output ──Name: ghcr.io/<you>/shoplite-api:sha-3f2a1c9MediaType: application/vnd.oci.image.index.v1+jsonDigest: sha256:9b1f... - Tag with the app version and
Would you run this in production?
- ☐Multi-stage build; no JDK, Maven, or source in the runtime image
- ☐Non-root user; read-only root filesystem compatible
- ☐Pinned base image versions (and ideally digests), rebuilt regularly for patches
- ☐Immutable tags (version + SHA);
latestnever deployed - ☐Vulnerability scan in the build
Stretch goals
- Build multi-arch (amd64 + arm64) with
docker buildx - Try a distroless or Chiseled Ubuntu base and compare size
- Build with Jib or Spring Boot Buildpacks and compare to your Dockerfile
Show it off
Résumé bullet
Containerised a Spring Boot service with a multi-stage, non-root Dockerfile, cutting image size from 812 MB to 241 MB and rebuild time to seconds via layer caching; images scanned and versioned by Git SHA.
Demo script
- Change one line of code and rebuild: show the CACHED dependency layers
- Run
docker historyto explain the layers - Show the Trivy report
Interview questions about this project
How do you make Docker builds for a Java app fast?
Why not run containers as root?