Packaging & Deployment

The executable jar

./mvnw clean package
java -jar target/demo-0.0.1-SNAPSHOT.jar

One file with your classes, the dependencies and a launcher. No application server.

java -jar app.jar --server.port=9000 --spring.profiles.active=prod
java -Dspring.profiles.active=prod -jar app.jar
SPRING_PROFILES_ACTIVE=prod java -jar app.jar

Container image, no Dockerfile

./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=demo:1.0
docker run -p 8080:8080 demo:1.0

Cloud Native Buildpacks pick a JDK, layer the image and set sane defaults.

Container image with a Dockerfile

Layered jars keep dependencies in their own layer, so a code change rebuilds only the last one:

FROM eclipse-temurin:25-jre AS builder
WORKDIR /app
COPY target/*.jar app.jar
RUN java -Djarmode=tools -jar app.jar extract --layers --launcher

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

Native images

./mvnw -Pnative native:compile          # needs GraalVM
./target/demo
JVMNative
startup~1 s~50 ms
memoryhighermuch lower
build timesecondsminutes
runtime reflectionfreeneeds hints

Worth it for functions and scale-to-zero workloads; usually not for a long-running service.

Configuration in production

  • profiles per environment, secrets from the environment or a secret manager
  • spring.jpa.hibernate.ddl-auto=validate plus Flyway migrations
  • actuator on a separate port: management.server.port=9001
  • structured logs: logging.structured.format.console=ecs
  • graceful shutdown so in-flight requests finish:
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s

Kubernetes essentials

readinessProbe:
  httpGet: { path: /actuator/health/readiness, port: 9001 }
livenessProbe:
  httpGet: { path: /actuator/health/liveness, port: 9001 }
resources:
  requests: { memory: 512Mi, cpu: "0.5" }
  limits:   { memory: 1Gi }

The JVM reads container limits by default — set the memory limit, not -Xmx, unless you have a reason.

Tip

management.endpoint.health.probes.enabled=true gives you the two probe endpoints; combined with graceful shutdown you get rolling deploys without dropped requests.

Release checklist

  • ./mvnw verify green, including integration tests
  • migrations tested against a copy of production data
  • no secrets in the image or the repository
  • health probes, metrics and logs reaching your platform
  • rollback path: previous image tag still deployable
  • dependency and base image versions patched

★ Exercises

  1. Build the jar and run it with the prod profile.
  2. Build an image with spring-boot:build-image and run it.
  3. Write a layered Dockerfile and compare the rebuild time after a one-line change.
  4. Enable graceful shutdown and watch a long request finish during SIGTERM.
  5. Add readiness and liveness probes, then make readiness fail on purpose.