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/| Option | Image size | When to choose |
|---|---|---|
bootBuildImage (buildpacks) | Larger, tuned base | No Dockerfile maintenance, fast path |
| Dockerfile with layered jar | Lean | You want control over the base image and user |
| Jib | Lean, daemonless | CI builds without a Docker daemon |
| Fat jar on a VM | n/a | Legacy 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"]MaxRAMPercentagemakes the heap follow the container limit rather than the host memory.ExitOnOutOfMemoryErrorlets 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=gracefulplus aspring.lifecycle.timeout-per-shutdown-phasedrains 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.Related
Observability with Actuator, logging and metrics Next steps: WebFlux, messaging and native images
Last refreshed 2026-09-18.