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
latesttags for production deployments. - Separate build and runtime identities and restrict access to the image registry.
11. Common issues and troubleshooting
| Symptom | Possible cause and checks |
|---|---|
no main manifest attribute | The copied JAR may not be the executable Spring Boot JAR. Check Maven packaging and the exact artifact name. |
| Container exits immediately | Review docker logs; verify the entrypoint, Java version, configuration, and required environment variables. |
Permission denied reading a file | Confirm ownership and permissions after switching to the non-root user. |
| Port is not reachable | Confirm the application listens on 0.0.0.0 inside the container, the correct port is published, and host/network rules permit traffic. |
| Image unexpectedly large | Inspect docker history; check whether build tools, local target/, source, or unnecessary files were copied into the final stage. |
| Dependency download repeats on every build | Reorder Dockerfile layers, use BuildKit caching, and check whether dependency files change frequently. |
| App is killed under load | Review container memory limits, JVM memory use, heap settings, and out-of-memory events. |
| Health check fails | Verify the endpoint, actuator exposure, authentication, and whether the check command exists in the image. |
| Works locally but fails in AWS | Check 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.
-
.dockerignoreexcludes 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
- Dockerfile reference
- Docker build best practices
- Multi-stage builds
- Docker build context and
.dockerignore - Docker security
- Spring Boot container images
- Spring Boot graceful shutdown
- Eclipse Temurin container images
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.