Containerizing a Spring Boot App
Package your app into a portable image with a Dockerfile , speed up rebuilds with layered jars , skip the Dockerfile entirely using buildpacks , and configure everything through environment variables .
Learn Containerizing a Spring Boot App in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Before You Start
You should be able to build a runnable Spring Boot jar and understand its profiles and external configuration . Basic familiarity with the command line and Docker concepts helps.
What You'll Learn
The Big Idea — A Shipping Container for Your App
💡 Analogy: A Docker image is a standardized shipping container : your app, its JRE, and everything it needs are sealed inside, so it runs the same on your laptop, a colleague's machine, or a cloud server. The Dockerfile is the packing list. Layered jars are like packing the heavy, unchanging machinery at the bottom and your daily paperwork on top — when only the paperwork changes, you don't repack the whole container. Buildpacks are a professional packing service that loads the container correctly for you. And the environment variables are the shipping label that tells the same container which port to dock at.
Build the container once; the label (env vars) decides where and how it runs.
1️⃣ Config From the Environment
Containers follow the twelve-factor rule: one immutable image, configured at runtime via environment variables . Spring Boot reads them automatically; the demo mimics that lookup-with-default.
2️⃣ Layered Jars and the Build Cache
Docker caches image layers by content. Put rarely-changing dependencies in their own layers and your frequently-changing app code last, so a routine code change only rebuilds the small top layer. The demo models that cache decision.
3️⃣ A Layered, Multi-Stage Dockerfile
Build with the JDK, run on a slim JRE. Extract the Spring Boot jar into layers and copy the least-changing ones first to maximize cache hits.
4️⃣ No Dockerfile Needed — Buildpacks
Spring Boot's spring-boot:build-image (Maven) and bootBuildImage (Gradle) use Cloud Native Buildpacks to produce an optimized, layered, non-root image — no Dockerfile to maintain.
🧠 Quick Recall
Answer: the application layer (your own code) — it changes most often, so it's the top, most-frequently-rebuilt layer.
Answer: use buildpacks via mvn spring-boot:build-image or ./gradlew bootBuildImage .
Answer: from environment variables at runtime (e.g. SPRING_DATASOURCE_URL binds to spring.datasource.url ).
🎯 YOUR TURN — Env Config With Defaults
Implement env(key, default) and print the resolved profile and port.
🧩 MINI-CHALLENGE — Simulate the Layer Cache
Given the first changed layer index, mark every layer from there onward as REBUILD and count them.
Common Errors
- ❌ Copying app code before dependencies. Every change busts the dependency cache. Fix: copy least-changing layers first.
- ❌ Shipping a full JDK at runtime. Bloated, larger attack surface. Fix: run on a slim JRE via multi-stage build.
- ❌ Baking config/secrets into the image. Not portable, leaks secrets. Fix: pass env vars at runtime.
- ❌ Running as root. A container escape is more dangerous. Fix: use a non-root user (buildpacks do this for you).
- ❌ Expecting EXPOSE to publish the port. It's only documentation. Fix: publish with docker run -p .
- ❌ Not pinning the base image tag. latest breaks reproducibility. Fix: pin a specific version (e.g. 21-jre ).
Pro Tips
- 💡 Prefer buildpacks ( spring-boot:build-image ) unless you need fine-grained Dockerfile control.
- 💡 Use a .dockerignore to keep build context small and fast.
- 💡 Wire /actuator/health as the container health check for orchestrators.
- 💡 Override config with env vars using Spring's relaxed binding — one image, many environments.
📋 Quick Reference
Concept
Syntax / idea
Note
base image
FROM eclipse-temurin:21-jre
Slim runtime
copy artifact
COPY target/app.jar app.jar
Or layered dirs
run command
ENTRYPOINT ["java","-jar",...]
Startup process
port doc
EXPOSE 8080
Publish with -p
layered jar
-Djarmode=layertools extract
Better caching
layer order
deps first, app last
Max cache hits
buildpacks (mvn)
spring-boot:build-image
No Dockerfile
buildpacks (gradle)
bootBuildImage
runtime config
docker run -e KEY=VALUE
Env vars
relaxed binding
SPRING_DATASOURCE_URL
-> spring.datasource.url
📚 Resources / Keep Learning
- 📘 Container Images — Spring Boot reference
- 📗 Spring Boot with Docker — Spring topical guide
- 📗 Multi-stage builds — Docker docs
❓ Frequently Asked Questions
🎉 Lesson Complete!
You can now write a multi-stage, layered Dockerfile, explain why layer order maximizes the build cache, build images without a Dockerfile using buildpacks, and configure the same image across environments with environment variables.
Next up: the Capstone — assembling a complete REST API from everything you've learned.
Practice quiz
What does a Dockerfile describe?
- A database schema
- The steps to build a container image
- A REST endpoint
- A unit test
Answer: The steps to build a container image. A Dockerfile is a recipe of instructions (FROM, COPY, ENTRYPOINT, etc.) that build a reproducible container image.
Which Dockerfile instruction sets the base image?
- RUN
- FROM
- CMD
- LABEL
Answer: FROM. FROM declares the base image the new image is built on, such as a JRE image for running a Spring Boot jar.
Which instruction defines the command that runs when the container starts?
- ENTRYPOINT
- WORKDIR
- ENV
- EXPOSE
Answer: ENTRYPOINT. ENTRYPOINT (or CMD) specifies the process the container runs, e.g. java -jar app.jar.
Why split a Spring Boot jar into layers in the image?
- To encrypt the jar
- To make the jar smaller than the source
- So unchanged layers (dependencies) are cached and only changed layers rebuild/ship
- Because Docker requires exactly four layers
Answer: So unchanged layers (dependencies) are cached and only changed layers rebuild/ship. Layered jars put rarely-changing dependencies in their own layers, so Docker's layer cache avoids re-shipping them on each rebuild.
In a layered Spring Boot jar, which layer changes most often?
- dependencies
- spring-boot-loader
- snapshot-dependencies
- application (your own classes/resources)
Answer: application (your own classes/resources). Your application code changes most frequently, so it belongs in the last, most-often-rebuilt layer.
What does Spring Boot's buildpacks support (mvn spring-boot:build-image) do?
- Runs your tests only
- Builds an optimized OCI image WITHOUT you writing a Dockerfile
- Deploys to production automatically
- Generates JavaDoc
Answer: Builds an optimized OCI image WITHOUT you writing a Dockerfile. spring-boot:build-image uses Cloud Native Buildpacks to produce a sensible, layered image with no hand-written Dockerfile.
A benefit of buildpacks over a hand-written Dockerfile is...
- They never produce images
- They require root by default
- They only work offline
- They apply best practices (layering, JRE choice, non-root) automatically
Answer: They apply best practices (layering, JRE choice, non-root) automatically. Buildpacks encode best practices — appropriate base image, layering, and a non-root user — so you don't maintain a Dockerfile.
How should configuration like the database URL be supplied to a container?
- Hard-coded in the Dockerfile
- Via environment variables at runtime
- Recompiled into the jar each time
- In a comment
Answer: Via environment variables at runtime. Twelve-factor config: pass settings as environment variables so the same image runs in any environment without rebuilding.
Spring Boot maps an env var SPRING_DATASOURCE_URL to which property?
- server.port
- logging.level
- spring.datasource.url
- info.app.name
Answer: spring.datasource.url. Relaxed binding maps SPRING_DATASOURCE_URL to spring.datasource.url, so env vars override properties.
Why prefer a JRE base image over a full JDK for the final runtime image?
- The JRE can't run jars
- A smaller runtime image (no compiler/tools) is leaner and has less attack surface
- The JDK is required at runtime
- It makes builds slower
Answer: A smaller runtime image (no compiler/tools) is leaner and has less attack surface. You compile with the JDK but only need a JRE to run; a slimmer runtime image is smaller and more secure.
Continue this course
- Previous: Caching with @Cacheable
- Next: Capstone: A Complete REST API