Spring Boot produces a self-contained executable JAR, which makes deployment conceptually simple — but the JVM and container memory limits don't always get along, and Spring's config layering catches people off guard. This guide covers a production-ready containerized deployment.
Build an efficient image with layered JARs
A naive COPY app.jar works but rebuilds the whole layer on every code change. Spring Boot supports layered JARs that separate dependencies (rarely change) from your code (changes constantly), giving you far better Docker layer caching.
# --- build stage ---
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw -DskipTests clean package
RUN java -Djarmode=layertools -jar target/*.jar extract
# --- runtime stage ---
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/dependencies/ ./
COPY --from=build /app/spring-boot-loader/ ./
COPY --from=build /app/snapshot-dependencies/ ./
COPY --from=build /app/application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]Use a JRE (not JDK) base for the runtime to keep the image lean.
The JVM-in-a-container memory trap
For years the JVM ignored cgroup memory limits and sized its heap based on the host's total RAM — leading to OOM kills inside containers. Modern JVMs (11+) are container-aware, but you should still set heap limits explicitly:
JAVA_OPTS=-XX:MaxRAMPercentage=75.0MaxRAMPercentage tells the JVM to use up to 75% of the container's memory limit for the heap, leaving headroom for metaspace, thread stacks, and off-heap buffers. Setting a fixed -Xmx works too, but the percentage adapts when you resize the container.
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]Externalized configuration
Spring Boot reads config from many sources with a defined precedence — environment variables override application.properties. Use that to keep secrets out of your JAR:
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD}
spring.jpa.hibernate.ddl-auto=validateSet ddl-auto to validate (or none) in production — never update or create, which mutate your schema unpredictably. Use Flyway or Liquibase for migrations instead.
Health probes with Actuator
Spring Boot Actuator gives you liveness and readiness endpoints for free:
management.endpoint.health.probes.enabled=true
management.endpoints.web.exposure.include=health,infoThis exposes /actuator/health/liveness and /actuator/health/readiness. Wire readiness into your platform's probe so traffic only routes once Spring has fully started and the database is reachable.
Migrations with Flyway
Drop SQL files in src/main/resources/db/migration named V1__init.sql, V2__add_users.sql, etc. Flyway runs them on startup and tracks applied versions. For larger teams, run Flyway as a discrete release step so app replicas don't race.
Deploying on PandaStack
- 1Create a PostgreSQL or MySQL database;
DATABASE_URL(and credentials) are injected automatically. - 2Connect your repo as a container app. PandaStack detects the Dockerfile and builds via rootless BuildKit; or buildpacks detect Maven/Gradle and Java if you skip the Dockerfile.
- 3Set
JAVA_OPTS,SPRING_PROFILES_ACTIVE=production, and DB env vars in the dashboard. - 4Push — live build logs stream and you get automatic SSL.
| Concern | Production setting |
|---|---|
| Image | Layered JAR, JRE runtime |
| Heap | -XX:MaxRAMPercentage=75.0 |
| Schema | ddl-auto=validate + Flyway |
| Probes | Actuator liveness/readiness |
| Profile | SPRING_PROFILES_ACTIVE=production |
Common pitfalls
ddl-auto=updatein production — silent, unpredictable schema drift.- No heap limit — OOM kills under container memory pressure.
- Exposing all Actuator endpoints —
/actuator/envand/actuator/heapdumpleak data; expose only what you need. - JDK in the runtime image — bloated image; use a JRE.
References
- Spring Boot container deployment: https://docs.spring.io/spring-boot/reference/packaging/container-images/dockerfiles.html
- Spring Boot Actuator: https://docs.spring.io/spring-boot/reference/actuator/index.html
- JVM container awareness (OpenJDK): https://www.eclipse.org/openj9/docs/xxusecontainersupport/
- Flyway documentation: https://documentation.red-gate.com/flyway
- Eclipse Temurin images: https://hub.docker.com/_/eclipse-temurin
---
PandaStack's free tier includes container apps and a managed database with DATABASE_URL injected automatically — push your Spring Boot repo and it builds, deploys, and connects. Get started at https://dashboard.pandastack.io