Docker and Java: Advanced Application Containerization

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Docker and Java: Advanced Level of Application Containerization

\n\n

Containerization of Java applications has become the de facto standard in modern development. However, basic knowledge of Docker (creating an image from openjdk:11 and running a container) no longer meets the requirements of production environments. At an advanced level, it is necessary to consider the specifics of JVM operation inside a container, optimize image sizes, manage memory, and configure monitoring. In this article, we will cover key techniques that will help you create efficient and secure Docker images for Java applications.

\n\n

1. Multi-Stage Build: Minimizing Image Size

\n\n

The classic "one Dockerfile — one image" approach often leads to bloated final artifacts. Build tools (Maven, Gradle, compiler) end up in the image, which are not needed at runtime. The solution is a multi-stage build. We split the process into stages: build and runtime.

\n\n

Example Dockerfile for a Spring Boot application using Maven:

\n\n
# Build stage\nFROM maven:3.8.6-openjdk-17 AS builder\nWORKDIR /app\nCOPY pom.xml .\nRUN mvn dependency:go-offline -B\nCOPY src ./src\nRUN mvn clean package -DskipTests\n\n# Runtime stage\nFROM eclipse-temurin:17-jre-alpine\nWORKDIR /app\nCOPY --from=builder /app/target/*.jar app.jar\nEXPOSE 8080\nENTRYPOINT ["java", "-jar", "app.jar"]
\n\n

As a result, the final image weighs ~150 MB instead of 600+ MB. Important: use jre images (not jdk) and minimal distributions (Alpine, Slim). For applications on Java 17+, consider jlink — creating a custom JRE with only the necessary modules.

\n\n

2. Configuring JVM for Container Operation

\n\n

For a long time, the JVM could not correctly determine the memory and CPU limits set for a container. Starting with Java 8u191 and Java 10, support for cgroups v1 was added (and with Java 15, cgroups v2). However, you should not rely solely on automation.

\n\n

Key parameters for production:

\n
    \n
  • -XX:+UseContainerSupport — enables container support (active by default in Java 10+).
  • \n
  • -XX:MaxRAMPercentage=75.0 — limits heap usage to 75% of the container's available memory. This leaves memory for metaspace, thread stacks, and system processes.
  • \n
  • -XX:ActiveProcessorCount=2 — explicit specification of the number of available processors if auto-detection fails.
  • \n
\n\n

Example of a complete ENTRYPOINT command:

\n\n
ENTRYPOINT ["java", \\\n    "-XX:+UseContainerSupport", \\\n    "-XX:MaxRAMPercentage=75.0", \\\n    "-XX:+ExitOnOutOfMemoryError", \\\n    "-jar", "app.jar"]
\n\n

Important: avoid fixed values for -Xmx and -Xms in a Docker environment. They do not adapt to changes in container limits during replication or scaling.

\n\n

3. Layer Optimization and Build Caching

\n\n

Each instruction in a Dockerfile creates a new layer. Docker caches layers that have not changed. To speed up repeated builds, arrange instructions from least to most frequently changed.

\n\n

Correct Dockerfile structure:

\n
    \n
  1. Install OS dependencies (apk add, apt-get).
  2. \n
  3. Copy build tool configuration files (pom.xml, build.gradle).
  4. \n
  5. Download dependencies (mvn dependency:go-offline).
  6. \n
  7. Copy source code.
  8. \n
  9. Build and copy the artifact.
  10. \n
\n\n

Additionally, you can use docker layer caching in CI/CD. For example, in GitHub Actions or GitLab CI, save the Maven cache between builds:

\n\n
# Example for GitLab CI\ncache:\n  key: ${CI_COMMIT_REF_SLUG}\n  paths:\n    - .m2/repository/\n    - .gradle/caches/
\n\n

Also consider using

Blogs

Book Recommendations