Java: Containerizing Applications for Consistent Deployments

Introduction

A Java application may work on a developer’s machine but fail in a test or production environment because the JDK, operating system libraries, configuration, or runtime limits differ.

Containers help package an application with a consistent runtime environment. They do not eliminate every environmental difference, but they make deployments more reproducible when the image and configuration are managed properly.

1. Build a reproducible application artifact

For a typical Spring Boot application, the build process produces an executable JAR:

./mvnw clean package

Run tests as part of the build rather than producing a deployable artifact from an unverified source tree. A reproducible build should also pin or control important build-tool and dependency versions.

Keep environment-specific secrets and configuration out of the JAR itself.

2. Create a simple container image

A multi-stage Dockerfile separates compilation from runtime:

FROM eclipse-temurin:21-jdk AS build
WORKDIR /app

COPY .mvn/ .mvn/
COPY mvnw pom.xml ./
RUN chmod +x mvnw
RUN ./mvnw -B dependency:go-offline

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

FROM eclipse-temurin:21-jre
WORKDIR /app

COPY --from=build /app/target/*.jar app.jar

USER 10001
EXPOSE 8080

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

This example targets Java 21 as an illustrative baseline. Select a runtime supported by your application and framework, and pin image versions according to your organization’s supply-chain policy.

The build stage skips tests in this illustrative Dockerfile because tests should already run in CI. In a pipeline that does not run them separately, remove that option and ensure the tests execute before the image is published.

3. Run as a non-root user

The container runs the Java process as a non-root user. This reduces privileges if the application is compromised.

Also minimize the runtime image, scan base images and dependencies, and rebuild regularly to incorporate relevant security updates. A small image is not automatically a secure image; provenance, maintenance, and vulnerability handling matter too.

4. Externalize configuration

Provide environment-specific values through environment variables, mounted configuration, or a secrets-management system.

Do not bake passwords, access tokens, or production credentials into the image. Treat container image layers as potentially inspectable, even if a secret was deleted in a later build step.

Verify that configuration is validated at startup and that missing required settings produce clear operational errors.

5. Account for memory and shutdown behavior

Container memory limits affect how much memory a Java process can use. Test the application with realistic CPU and memory limits rather than assuming it has access to the host machine’s resources.

Configure health checks and graceful shutdown so the application can stop accepting new work and finish or safely cancel in-flight requests where appropriate.

6. Test the exact image you deploy

Run integration tests against the built container, not only against a local process. Confirm that ports, permissions, health endpoints, certificates, and external service connections work in the target environment.

Promote the same immutable image through environments where possible, changing configuration rather than rebuilding different binaries for each deployment stage.

Conclusion

Containerization makes Java deployments more consistent when builds are reproducible, images are maintained, and runtime configuration is handled securely. Treat the container as a production artifact: test it, scan it, observe it, and promote it through a controlled release process.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *