RizTech Academy logo
RizTech Academy
Configuration and DeploymentLesson 3 of 435 min

Containerising a Spring Boot application

A Spring Boot application packages as a single self-contained jar, which makes it a natural fit for containers: a Docker image bundles that jar with its exact Java runtime, so it runs identically on your laptop, in CI, and in production. Containers are the common way to deploy Spring Boot today. This lesson is how to containerise Dakiya well — the Dockerfile, the layered-jar optimisation, and the mistakes to avoid.

Why containers suit Spring Boot

Spring Boot's executable jar (with its embedded server) is already self-contained — java -jar dakiya.jar runs the whole application, no separate server to install. A container adds the one thing the jar still assumes: a specific Java runtime. The image bundles the jar and the exact JDK/JRE, so "works on my machine" disappears — the same image, with the same Java, runs everywhere Docker runs. Combined with configuration from the environment (the configuration lesson), one image runs in every environment with different config fed in. That reproducibility is why containers and Spring Boot go together.

A Dockerfile for Spring Boot

A straightforward, correct Dockerfile:

FROM eclipse-temurin:17-jre                        # a specific JRE — matches the app's Java version

WORKDIR /app
COPY target/dakiya-0.0.1-SNAPSHOT.jar app.jar      # the built jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

You build the jar first (./mvnw package), then build the image. The choices that matter:

  • A specific base image (eclipse-temurin:17-jre, not latest) — pin the Java version so the image is reproducible, and use a JRE (runtime) not a full JDK for a smaller image (you do not compile inside the container).
  • EXPOSE/ENTRYPOINT — document the port and run the jar.

This works, but a naive "copy the fat jar and run it" misses an important optimisation, below.

The layered-jar optimisation

Docker images are built in layers, and layers are cached — a layer only rebuilds if it or something before it changed. A Spring Boot fat jar is mostly dependencies (Spring, libraries — hundreds of MB) and a little your code (a few KB). If you copy the whole jar as one layer, every code change re-copies all the dependencies — slow builds, huge image pushes. Spring Boot solves this with layered jars: it can split the jar into layers (dependencies, then your code) so the dependency layer is cached and only your tiny code layer changes on a normal build.

The modern, easiest way to get this is Spring Boot's built-in image building — no Dockerfile at all:

./mvnw spring-boot:build-image

This uses Cloud Native Buildpacks to produce an optimised, layered, production-sensible image automatically (right base, layered, non-root user, JVM tuned for containers). For most projects this is the recommended approach — it gives you a good image without hand-writing and maintaining a Dockerfile. If you do write a Dockerfile, use the layered-jar extraction (java -Djarmode=layertools -jar app.jar extract) and copy the layers separately, so dependencies cache and only code rebuilds. Either way, the goal is: dependencies in a cached layer, your code in a small top layer — fast rebuilds, small pushes.

The container mistakes to avoid

The recurring ones for Spring Boot:

  • latest base images — non-reproducible; pin the Java version.
  • A full JDK for runtime — bloats the image; use a JRE (or a buildpack image, which does this for you).
  • Secrets baked into the image — never COPY a .env with real secrets or hard-code them; the image is shareable and the secret travels with it. Pass secrets as environment variables at run time.
  • Not copying the layers separately (in a hand-written Dockerfile) — losing the dependency-layer cache, so every build is slow.
  • Running as root — run as a non-root user (buildpacks do this automatically) for security.
  • A .dockerignore missing — copying target/ build cruft, .git, etc. into the build context bloats it; use a .dockerignore.

Most of these vanish if you use spring-boot:build-image, which is why it is the pragmatic default.

Configuration and the container

The container runs the same image everywhere; configuration comes in at run time, not build time:

docker run -e SPRING_PROFILES_ACTIVE=prod \
           -e DATABASE_URL=postgres://... \
           -e DAKIYA_JWT_SECRET=... \
           -p 8080:8080 dakiya:latest

Environment variables map to Spring properties (SPRING_PROFILES_ACTIVE, and Spring's relaxed binding turns DATABASE_URL-style names into properties), so the same image becomes "the production Dakiya" purely by the env vars you pass. And for a full local stack, a docker-compose file runs the app image alongside a PostgreSQL container — the way to develop against the real database (the postgres lesson) with one command. The principle: build one image, feed configuration and secrets at run time as environment variables — the container is the artifact, the environment supplies the specifics.

Check your work

Why containers suit Spring Boot. The executable jar (embedded server) is self-contained; the image adds the exact Java runtime, so the same image runs identically everywhere — reproducibility.

The Dockerfile. Pin a specific JRE base (not latest, not a full JDK), copy the jar, ENTRYPOINT java -jar. Build the jar first.

Layered jars. Images are cached in layers; a fat jar copied as one layer re-copies all dependencies on every code change. Use Spring Boot's spring-boot:build-image (buildpacks — optimised, layered, non-root, no Dockerfile) or extract layered-jar layers in a Dockerfile so dependencies cache and only code rebuilds.

Mistakes. latest bases, full JDK for runtime, secrets baked in (pass as run-time env vars), not layering (slow builds), running as root, missing .dockerignore — most avoided by build-image.

Configuration at run time. Pass SPRING_PROFILES_ACTIVE, DATABASE_URL, secrets as -e env vars; the same image becomes the production app via env vars. docker-compose runs the app + Postgres locally.

Practice

  1. Build the jar (./mvnw package) and write a Dockerfile with a pinned JRE base; build and run the image, confirming the app starts.
  2. Run ./mvnw spring-boot:build-image and compare the resulting image (layered, non-root) with your hand-written one.
  3. Change one line of code and rebuild both a naive single-layer image and a layered one; compare rebuild time.
  4. Run the container passing SPRING_PROFILES_ACTIVE=prod and a DATABASE_URL as env vars; confirm the config applies.
  5. Reason about why baking a .env with real secrets into the image is dangerous, and pass them as env vars instead.
  6. Write a docker-compose with the app image and a postgres service; run the stack and confirm the app connects.

Official documentation

Next: deploying and the production checklist.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship