AWS

Dockerize a Spring Boot Application: Dockerfile, Image Size and Best Practices

By Utility Zone · 2026-10-03T16:25:55.460066

AWS & Java engineering series — Containers & DevOps
Audience: Java developers, Spring Boot engineers, tech leads, and DevOps engineers
Goal: Build a secure, repeatable, and reasonably small container image for a Spring Boot application.

1. Why Dockerize a Spring Boot application?

Docker packages an application and its runtime dependencies into an image that can run consistently across supported environments. For a Spring Boot service, a container image can simplify local testing, CI/CD, and deployment to Amazon ECS, Kubernetes, or another container platform.

A good container build should be:

  • Repeatable: the same source and build inputs produce a predictable artifact.
  • Small enough: avoid shipping build tools and unnecessary files in the runtime image.
  • Secure: run as a non-root user, keep secrets out of the image, and patch the base image.
  • Observable: write logs to standard output/error where practical.
  • Operationally clear: configure health checks, resource limits, and graceful shutdown at the platform level.

Smaller is not automatically safer or faster in every situation. Choose the runtime base image based on compatibility, security support, debugging needs, and measured performance—not image size alone.

2. Example project structure

This guide assumes a Maven-based Spring Boot project:

my-spring-app/
├── src/
├── pom.xml
├── mvnw
├── .mvn/
├── Dockerfile
├── .dockerignore
└── README.md

Build the application locally first:

./mvnw clean package

On Windows, use mvnw.cmd clean package. Confirm the executable JAR is created under target/. If your build creates multiple JARs (for example, an original JAR and a repackaged Spring Boot JAR), make sure the Docker build copies the executable Spring Boot artifact.

3. Start with a simple Dockerfile

A single-stage Dockerfile is easy to understand and can be suitable when the build artifact is produced by CI and only the JAR is copied into the image.

FROM eclipse-temurin:21-jre

WORKDIR /app

RUN useradd --system --uid 10001 --create-home appuser

COPY --chown=appuser:appuser target/*.jar app.jar

USER 10001

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app/app.jar"]

What each instruction does

  • FROM: selects the Java runtime base image. Use a currently supported JRE image and pin a specific version or digest according to your image update policy.
  • WORKDIR: sets the working directory.
  • RUN useradd: creates an unprivileged runtime user. The command above is suitable for Debian/Ubuntu-style images; other base images may use different user-management tools or already provide a non-root user.
  • COPY --chown: copies the executable JAR with ownership set to the application user.
  • USER: runs the Java process without root privileges.
  • EXPOSE: documents the container's listening port; it does not publish the port by itself.
  • ENTRYPOINT: uses exec form so Java receives container signals directly.

Compatibility note: useradd is not available in every base image. Verify the chosen image's OS and user-management tools before using this exact Dockerfile. For production, prefer a supported, patched runtime image and a pinned version/digest.

4. Use a multi-stage build when the image should build the application

A multi-stage build keeps Maven and source files out of the final runtime image. It also makes the build process easier to reproduce in CI, although dependency downloads and build cache behavior should be optimized separately.

# Build stage
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /workspace

COPY pom.xml .
COPY .mvn/ .mvn/
COPY mvnw .
RUN chmod +x mvnw

# Download dependencies separately to improve layer caching
RUN ./mvnw -B -ntp dependency:go-offline

COPY src/ src/
RUN ./mvnw -B -ntp clean package -DskipTests

# Runtime stage
FROM eclipse-temurin:21-jre
WORKDIR /app

RUN useradd --system --uid 10001 --create-home appuser

COPY --from=build --chown=appuser:appuser \
     /workspace/target/*.jar /app/app.jar

USER 10001
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

This is a teaching example, not a universal drop-in file. Adapt the Maven wrapper setup to your repository. If the wrapper requires files not copied into the build stage, add them. Prefer running tests in CI or in a build stage rather than silently skipping them in every workflow. If the project creates multiple JARs, replace the wildcard with the exact executable JAR name.

Why multi-stage builds help

The final stage contains only the runtime image and the JAR copied from the build stage. Maven, source files, and intermediate build outputs are not carried into the final image unless explicitly copied. Multi-stage builds do not automatically make the JAR itself smaller; use dependency analysis and build configuration to address that.

5. Add a .dockerignore file

The Docker build context should contain only files needed by the build. A .dockerignore file reduces accidental copies and can improve build performance.

.git
.gitignore
.idea
.vscode
*.iml

target/
build/
out/

.env
.env.*
*.pem
*.key
secrets/

If the Dockerfile copies a locally built target/*.jar, do not ignore target/ for that specific workflow. For the multi-stage example above, target/ is generated inside the Docker build, so ignoring local target/ is appropriate. Never place credentials or private keys in the build context in the first place; ignore rules are an additional safeguard, not a secret-management system.

6. Build, inspect, and run the image

Build the image from the project root:

docker build -t my-spring-app:local .

Inspect the image size and layers:

docker image ls my-spring-app
docker history my-spring-app:local

Run the container:

docker run --rm --name my-spring-app \
  -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=local \
  my-spring-app:local

Then call a known endpoint:

curl -i http://localhost:8080/actuator/health

The health endpoint is available only if Spring Boot Actuator is included and configured to expose it. Do not expose management endpoints publicly without authentication and network restrictions.

For a quick log check:

docker logs -f my-spring-app

With --rm, Docker removes the container when it exits. To inspect it after a failure, omit --rm during troubleshooting.

7. Practical ways to reduce image size

Choose the runtime image carefully

Use a supported JRE image instead of a full JDK when the application only needs to run. Compare candidate images by size, vulnerability results, startup behavior, native-library compatibility, and patch cadence. Distroless or minimal images can reduce included utilities, but may make interactive debugging harder.

Use multi-stage builds

Keep compilers, Maven, source code, and intermediate outputs in the build stage. Copy only the final executable artifact and required runtime assets into the runtime stage.

Improve layer caching

Copy build descriptors before application source files so dependency layers can be reused when only source code changes. BuildKit cache mounts can further reduce repeated downloads in CI, but require compatible builder configuration.

Review application dependencies

Remove unused libraries, avoid duplicate dependencies, and inspect the dependency tree:

./mvnw dependency:tree

Do not remove dependencies merely to achieve a smaller image without testing behavior. Spring Boot packaging and frameworks may require libraries that are not obvious from direct imports.

Avoid unnecessary files

Use .dockerignore; do not copy .git, local IDE files, test reports, credentials, or local build output into the runtime image unless explicitly needed.

Be careful with Alpine and native dependencies

Alpine-based images use musl rather than glibc. Some Java dependencies, agents, native libraries, or diagnostic tools may behave differently. Test compatibility before selecting Alpine solely because its image size looks smaller.

Measure instead of guessing

Record the baseline image size and build time. Change one thing at a time, then compare:

  • compressed registry size and local image size,
  • vulnerability scan results,
  • build duration and cache hit rate,
  • startup time and memory use,
  • application integration-test results.

8. Runtime configuration and secrets

Keep environment-specific configuration outside the image. Spring Boot can read environment variables, command-line arguments, mounted configuration files, or external configuration sources.

Example:

docker run --rm -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=prod \
  -e SERVER_PORT=8080 \
  my-spring-app:local

Do not bake database passwords, API tokens, private keys, or AWS access keys into a Dockerfile or image layer. Use your deployment platform's secret-injection mechanism or a secret manager such as AWS Secrets Manager or Systems Manager Parameter Store. Remember that environment variables may be visible to privileged operators and diagnostic tooling; follow your organization's secret-handling policy.

For AWS workloads, use IAM roles and the AWS SDK's default credentials provider chain rather than embedding static AWS keys in the container.

9. Health checks, graceful shutdown, and JVM settings

A container being running does not guarantee that the Spring Boot application is ready to serve requests. Configure health checks using the orchestrator's readiness and liveness features where available. Keep liveness checks focused on whether the process should be restarted; readiness checks should indicate whether it should receive traffic.

If you add a Docker HEALTHCHECK, ensure the runtime image contains the command it invokes. Minimal or distroless images may not include curl, wget, or a shell. Orchestrators such as ECS and Kubernetes have their own health-check mechanisms and may be a better fit.

Use exec-form ENTRYPOINT as shown above so the Java process receives signals. Configure Spring Boot graceful shutdown and allow enough termination grace time in the deployment platform. Avoid hard-coding heap settings without knowing the container memory limit and JVM behavior. Modern Java versions are container-aware, but memory should still be load-tested under the real resource limits.

10. Security and maintenance practices

  • Run as a non-root user.
  • Use supported base images and rebuild regularly for security updates.
  • Pin base image versions or digests according to your reproducibility policy.
  • Scan images in CI and address vulnerabilities according to risk and exploitability.
  • Do not run privileged containers or mount the Docker socket into the application container without a strong operational reason.
  • Use a read-only root filesystem where compatible, with explicit writable mounts for required paths.
  • Drop unnecessary Linux capabilities and configure resource limits at the runtime platform.
  • Sign or attest build artifacts if your software supply-chain process supports it.
  • Keep build secrets out of image layers; use BuildKit secret mounts for build-time secrets when needed.
  • Avoid relying on latest tags for production deployments.
  • Separate build and runtime identities and restrict access to the image registry.

11. Common issues and troubleshooting

SymptomPossible cause and checks
no main manifest attributeThe copied JAR may not be the executable Spring Boot JAR. Check Maven packaging and the exact artifact name.
Container exits immediatelyReview docker logs; verify the entrypoint, Java version, configuration, and required environment variables.
Permission denied reading a fileConfirm ownership and permissions after switching to the non-root user.
Port is not reachableConfirm the application listens on 0.0.0.0 inside the container, the correct port is published, and host/network rules permit traffic.
Image unexpectedly largeInspect docker history; check whether build tools, local target/, source, or unnecessary files were copied into the final stage.
Dependency download repeats on every buildReorder Dockerfile layers, use BuildKit caching, and check whether dependency files change frequently.
App is killed under loadReview container memory limits, JVM memory use, heap settings, and out-of-memory events.
Health check failsVerify the endpoint, actuator exposure, authentication, and whether the check command exists in the image.
Works locally but fails in AWSCheck architecture, environment variables, IAM role, networking, secret injection, resource limits, and platform-specific health checks.

12. Production-ready checklist

  • Docker build succeeds from a clean checkout.
  • Tests run in CI before the image is promoted.
  • Final image contains only required runtime files.
  • Image size, startup time, and memory usage have been measured.
  • .dockerignore excludes irrelevant files and local secrets.
  • Container runs as non-root.
  • Base image is supported, patched, and pinned appropriately.
  • No secrets are embedded in the image.
  • Logs go to standard output/error or are collected intentionally.
  • Health checks and graceful shutdown are configured at the right layer.
  • Image scanning and registry access controls are part of CI/CD.
  • Resource limits and rollback procedures are documented.

13. Key takeaways

A practical Spring Boot container strategy separates build and runtime stages, keeps the build context clean, runs the application as a non-root user, and externalizes configuration and secrets. Image size matters, but it should be balanced with compatibility, security updates, observability, and operational support.

Start with a straightforward Dockerfile, measure the result, then optimize based on image history, dependency analysis, and runtime testing. Treat the container image as a production artifact: version it, scan it, promote it through environments, and keep the build reproducible.

References


Educational example: verify the base image, user-management commands, artifact name, health checks, and runtime limits for your project before using these snippets in production.