Packaging and deploying a Spring Boot service

Layered jars, Dockerfiles and buildpacks, externalised configuration, container-aware memory settings and safe rolling updates.

Building the artefact

# layered jar: dependencies change rarely, code changes often
./gradlew bootJar
java -Djarmode=tools -jar build/libs/orders-0.0.1.jar extract --destination build/extracted

# inspect the layers
unzip -l build/libs/orders-0.0.1.jar | head -20

# run with an external config file and an explicit profile
java -jar orders.jar --spring.profiles.active=prod \
  --spring.config.additional-location=file:/etc/orders/
OptionImage sizeWhen to choose
bootBuildImage (buildpacks)Larger, tuned baseNo Dockerfile maintenance, fast path
Dockerfile with layered jarLeanYou want control over the base image and user
JibLean, daemonlessCI builds without a Docker daemon
Fat jar on a VMn/aLegacy environments, no container runtime
⚠️
Do not bake secrets or environment-specific values into the image. The same artefact must run in staging and production with different injected configuration, otherwise you are testing an artefact you do not ship.

A production Dockerfile

FROM eclipse-temurin:21-jre-alpine AS runtime

RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --chown=app:app build/libs/*.jar app.jar

USER app
EXPOSE 8080
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"

ENTRYPOINT ["java","-jar","/app/app.jar"]
  • MaxRAMPercentage makes the heap follow the container limit rather than the host memory.
  • ExitOnOutOfMemoryError lets the orchestrator restart a broken process instead of serving errors from a thrashing JVM.
  • Run as a non-root user and copy only the jar - build tools and sources belong in the builder stage.
  • Use a JRE base image, not a JDK: it is smaller and has fewer components to patch.

Startup, probes and rollout

# Kubernetes excerpt
livenessProbe:
  httpGet: { path: /internal/health/liveness, port: 8081 }
  initialDelaySeconds: 60
  failureThreshold: 3
readinessProbe:
  httpGet: { path: /internal/health/readiness, port: 8081 }
  periodSeconds: 5
lifecycle:
  preStop:
    exec: { command: ["sh","-c","sleep 5"] }   # let the LB deregister first
  • Liveness must never check downstream dependencies, or a slow database restarts every healthy instance.
  • server.shutdown=graceful plus a spring.lifecycle.timeout-per-shutdown-phase drains in-flight requests.
  • Run Flyway migrations before the rollout, not inside it, so a bad migration stops the deploy.
  • Keep one previous image tagged and pin your configuration to the environment - rollback is then a redeploy of a known artefact.

FAQ

What should the health endpoint check?
Readiness may include a cheap dependency check when the instance genuinely cannot serve traffic without it; liveness should only prove the process is responsive. Getting this backwards turns a dependency blip into a restart loop.
How do I set the heap size in a container?
Leave it to container-aware defaults, or set a percentage with -XX:MaxRAMPercentage and leave headroom for metaspace, threads and direct buffers. A fixed -Xmx larger than the container limit gets the process killed.

Observability with Actuator, logging and metrics Next steps: WebFlux, messaging and native images

Last refreshed 2026-09-18.