Docker Images, Kubernetes, and External Configuration

A Spring Boot application is easy to run with java -jar, but production rarely runs one hand-started process on one machine. This lesson shows how to turn the bootable jar into a Docker image, let Kubernetes create and replace containers, and move environment-specific values out of the artifact. The intended outcome is simple: the same application image can move from a developer machine to a cluster while ports, credentials, feature values, and health checks are supplied by the runtime environment.

In the production engineering section of this Spring Boot course, this topic is the bridge between application code and the platform that operates it. You still design controllers, services, data access, and Actuator endpoints in Spring Boot, but the release unit becomes an image and the operational contract is expressed through Kubernetes objects and external configuration.

What the Image Contains

A Docker image is a filesystem plus metadata. Each instruction in a Dockerfile usually creates a layer, and layers are reused by digest when their contents are unchanged. For a Spring Boot service, the image normally contains a Java runtime, the application jar or extracted jar layers, exposed port metadata, and the command that starts the JVM. The container created from that image is not a miniature virtual machine. It is an isolated process with its own filesystem view, environment variables, network namespace, and resource limits enforced by the host.

Spring Boot’s executable jar is already structured for this model. The jar contains application classes, dependency jars, a launcher, and metadata. When you copy the whole jar into one image layer, every code change invalidates that layer. When you extract the jar into dependency and application layers, dependency layers can stay cached while only application code changes. That usually makes builds and image pulls faster for services whose dependencies change less often than business code.

External Configuration Internals

Spring Boot builds an Environment from ordered property sources. Values can come from packaged application.properties or YAML, command-line arguments, Java system properties, OS environment variables, config tree files, profile-specific files, and other sources added by the application. Binding then maps those string values onto typed objects such as @ConfigurationProperties. This is why an image can be immutable while behavior still changes per environment: the image contains default configuration, and the deployment supplies overrides.

The ordering matters. A default inside the jar should be safe for local development, but a value supplied by Kubernetes as an environment variable can override it at runtime. Spring’s relaxed binding translates names across common forms: catalog.page-size, catalog.pageSize, and CATALOG_PAGE_SIZE can address the same logical property. For secrets, the important distinction is not whether Spring can read the value, but how the platform stores, displays, rotates, and audits it.

Kubernetes Objects and Control Loops

Kubernetes does not run a Dockerfile directly. A Deployment declares the desired number of matching Pods and the Pod template to create. A ReplicaSet tracks the Pods for a particular template revision. A Pod contains one or more containers that share a network identity and can share volumes. The scheduler places the Pod on a node, and the node agent starts the containers with the configured image, environment, probes, volumes, and limits.

The most important internal idea is reconciliation. You submit desired state; controllers continuously compare actual state to desired state and take action. If a Pod crashes, a replacement is created. If you change the image tag or an environment variable in the Pod template, Kubernetes creates a new ReplicaSet and performs a rollout. If your label selector is wrong, the controller can no longer find the Pods you meant it to own. If your readiness probe fails, the Pod can remain alive but receive no Service traffic.

Configuration Anatomy

The moving pieces are small but precise. A Dockerfile describes image construction. Spring configuration names the properties the application reads. A ConfigMap supplies non-secret values. A Secret supplies sensitive values, though it still requires cluster-level controls. A Deployment wires those values into containers and defines probes and resource budgets. A Service gives the selected Pods a stable in-cluster address.

The simplest image copies the jar and starts it:

FROM eclipse-temurin:21-jre
WORKDIR /workspace
COPY target/catalog-service.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/workspace/app.jar"]

This works and is easy to understand. Its trade-off is that the complete jar is one application layer, so cache reuse is coarse. A layered image separates stable dependencies from frequently changing classes:

FROM eclipse-temurin:21-jre AS builder
WORKDIR /workspace
COPY target/catalog-service.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

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

The application can define defaults that remain useful outside Kubernetes:

server.port=${PORT:8080}
spring.application.name=catalog-service
catalog.currency=${CATALOG_CURRENCY:USD}
catalog.page-size=${CATALOG_PAGE_SIZE:20}
management.endpoints.web.exposure.include=health,info
management.endpoint.health.probes.enabled=true

Here PORT, CATALOG_CURRENCY, and CATALOG_PAGE_SIZE are runtime override points. If no environment variable exists, Spring uses the value after the colon. Actuator probe endpoints are enabled so Kubernetes can distinguish startup, liveness, and readiness behavior.

Example 1: Local Container Override

Start with the smallest useful deployment unit: one image and one container on your machine. Build the jar, build the image, pass one environment variable, and query the application.

./mvnw -DskipTests package
docker build -t catalog-service:lab .
docker run --rm -p 8080:8080 -e CATALOG_CURRENCY=GBP catalog-service:lab
curl -s http://localhost:8080/actuator/health/readiness

The deterministic part is the readiness endpoint. When the application has started and the readiness state is accepting traffic, the response contains UP. If the application exposes an endpoint that returns the configured currency, it should now report GBP, proving that the value came from the container environment instead of the packaged default. This example teaches the boundary: the jar did not change, but its runtime environment did.

Example 2: ConfigMap and Secret

Next, move runtime values into Kubernetes resources. Non-secret values belong in a ConfigMap. Passwords, tokens, and private keys belong in a Secret, with the understanding that Kubernetes Secrets are an API object and must be protected by RBAC, encryption at rest, and careful logging practices.

apiVersion: v1
kind: ConfigMap
metadata:
  name: catalog-config
data:
  currency: EUR
  CATALOG_PAGE_SIZE: "50"
---
apiVersion: v1
kind: Secret
metadata:
  name: catalog-db
type: Opaque
stringData:
  password: local-dev-password

The expected behavior is that Pods referencing catalog-config receive EUR as CATALOG_CURRENCY and 50 as CATALOG_PAGE_SIZE. The password is available to the process as SPRING_DATASOURCE_PASSWORD only when the Deployment asks for that key. Changing these objects does not automatically rebuild the image. Environment variables are captured when the container starts, so a Deployment rollout is usually needed before running Pods see new values.

Example 3: Deployment, Probes, and Service

A Deployment ties image, config, probes, and resource limits together:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: catalog-service
  template:
    metadata:
      labels:
        app: catalog-service
    spec:
      containers:
        - name: app
          image: ghcr.io/example/catalog-service:1.0.0
          ports:
            - containerPort: 8080
          env:
            - name: CATALOG_CURRENCY
              valueFrom:
                configMapKeyRef:
                  name: catalog-config
                  key: currency
            - name: SPRING_DATASOURCE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: catalog-db
                  key: password
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              memory: 512Mi

A Service routes stable traffic to the Pods whose labels match its selector:

apiVersion: v1
kind: Service
metadata:
  name: catalog-service
spec:
  selector:
    app: catalog-service
  ports:
    - port: 80
      targetPort: 8080

With two replicas, Kubernetes tries to keep two ready Pods. During a normal rolling update, a new Pod must pass readiness before it receives traffic. If the JVM process exits, liveness eventually causes replacement. If the process is alive but the database-dependent readiness check fails, the Pod can stay running while being removed from Service endpoints. That difference prevents Kubernetes from restarting a process just because a dependency is temporarily unavailable.

Design Choices and Trade-offs

Use immutable image tags or digests for repeatable releases. The tag latest is convenient in a demo but ambiguous in incident response because two nodes may pull different image contents at different times. A unique tag tied to a commit or build plus the immutable digest gives operators something exact to inspect and roll back.

Choose between whole-jar and layered images based on build and pull behavior. Whole-jar images are simpler and often acceptable for small services. Layered images add build complexity but can reduce transfer time when dependencies are stable. Distroless or minimal runtime images reduce package surface area, but they also remove debugging tools. That is usually a good production trade if logs, metrics, exec access policy, and emergency debugging procedures are planned.

Prefer environment variables for a modest number of scalar settings. Use mounted files or config trees when values are numerous, multiline, or need file semantics such as certificates. Keep defaults conservative. A packaged default can be convenient, but it should not accidentally point production traffic to a developer database or disable authentication.

Failure Modes and Troubleshooting

Image pull failure. The symptom is a Pod stuck in ImagePullBackOff or ErrImagePull. Common causes are a wrong image name, missing registry credentials, a private registry policy, or a tag that was never pushed. Diagnose with kubectl describe pod and inspect the events. Correct the image reference, create the image pull secret, or push the missing image, then restart the rollout.

Application starts locally but not in the cluster. The symptom is CrashLoopBackOff. Causes include a missing required property, a wrong database URL, insufficient memory, or a command that points to the wrong main class. Check kubectl logs for the previous container and compare effective environment variables with the Deployment. Correct the ConfigMap, Secret, resource limit, or entrypoint. If memory is the problem, fix both JVM memory settings and container limits rather than only increasing one number blindly.

Pod is running but receives no traffic. The symptom is that kubectl get pods shows Running, but the Service has no endpoints or requests time out. Causes include a readiness probe path that returns non-2xx, a selector that does not match Pod labels, or the application listening on a different port than targetPort. Diagnose with kubectl get endpoints, kubectl describe service, and a direct port-forward to the Pod. Fix labels, ports, or the readiness endpoint.

Configuration change appears ignored. The symptom is that ConfigMap or Secret data changed, but the app still uses the old value. If the value is injected as an environment variable, the running process will not see the change. Diagnose by inspecting the Pod creation time and the environment in a newly created Pod. Roll the Deployment or mount configuration as files and design the application to reload them deliberately.

Security, Performance, and Reliability

Security begins before the cluster. Build images from trusted base images, keep build credentials out of final layers, scan for known vulnerabilities, and avoid writing secrets into Dockerfile instructions. In Kubernetes, scope service accounts, restrict who can read Secrets, and avoid dumping full environments in logs. Spring Boot error messages and configuration reports are useful locally, but production logging should avoid exposing secret values.

Performance is affected by image size, startup time, JVM memory ergonomics, probe timings, and CPU throttling. A small image pulls faster during scale-out. Reasonable resource requests help the scheduler place Pods; limits prevent one service from consuming the node. Readiness probes should reflect whether the app can serve traffic, while liveness probes should be conservative enough not to kill a slow but recovering JVM.

Reliability comes from predictable replacement. Store state outside the container filesystem unless it is disposable. Make shutdown graceful so Spring can stop accepting work and finish in-flight requests before Kubernetes terminates the container. Pair rolling updates with readiness checks and use rollback when the new ReplicaSet increases errors.

Hands-on Lab

Prerequisites: a Spring Boot web application with Actuator on the classpath, a working jar named target/catalog-service.jar, Docker or a compatible image builder, kubectl, and access to a local or remote Kubernetes cluster. If your cluster cannot pull local images, push the image to a registry and update the Deployment image reference.

Steps: create the Dockerfile, add the Spring properties shown earlier, build the jar, build and run the image locally with a different CATALOG_CURRENCY, then create k8s/config.yaml, k8s/deployment.yaml, and k8s/service.yaml from the examples. Apply them in an isolated namespace and wait for rollout:

kubectl create namespace boot-lab
kubectl -n boot-lab apply -f k8s/config.yaml
kubectl -n boot-lab apply -f k8s/deployment.yaml
kubectl -n boot-lab apply -f k8s/service.yaml
kubectl -n boot-lab rollout status deployment/catalog-service
kubectl -n boot-lab port-forward service/catalog-service 8080:80
curl -s http://localhost:8080/actuator/health/readiness
kubectl delete namespace boot-lab

Verification: rollout status should report a successful deployment. The readiness curl should return a health document whose status is UP. kubectl -n boot-lab get endpoints catalog-service should show at least one endpoint. To verify configuration, expose or log a non-secret property such as currency and confirm that the Kubernetes value overrides the packaged default. Cleanup is the final namespace deletion command; it removes the Deployment, Pods, Service, ConfigMap, and Secret created for the lab.

Assessment Exercises

  1. Your team changes CATALOG_PAGE_SIZE in a ConfigMap and nothing changes in the running app. Explain why, then propose two correction strategies and the operational trade-off of each.
  2. A Pod is healthy according to liveness but absent from the Service endpoints. List the Kubernetes objects and Spring Boot endpoints you would inspect, in order, and what each result would prove.
  3. Compare whole-jar and layered Docker images for a service with large stable dependencies and frequent controller changes. Which layers should change during a typical code-only release?
  4. Design a configuration scheme for database password rotation without rebuilding the image. Include how you would verify that the new Pods use the new credential and how you would roll back.
  5. A rolling update causes repeated restarts after thirty seconds. Decide whether you would first adjust probes, resource limits, or application startup behavior, and justify the diagnostic evidence you need.

Summary

Docker images package a Spring Boot service into a repeatable runtime filesystem and startup command. Kubernetes turns that image into reconciled Pods, rollouts, probes, Services, and resource budgets. External configuration lets the same image behave correctly in different environments by feeding Spring Boot’s property binding system from runtime sources. The best design keeps the image immutable, configuration explicit, secrets controlled, probes meaningful, and rollback straightforward.