Docker and Java: Advanced Level of Application Containerization
\n\nContainerization 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.
1. Multi-Stage Build: Minimizing Image Size
\n\nThe 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\nExample 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\nAs 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.
2. Configuring JVM for Container Operation
\n\nFor 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\nKey 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
Example of a complete ENTRYPOINT command:
\n\nENTRYPOINT ["java", \\\n "-XX:+UseContainerSupport", \\\n "-XX:MaxRAMPercentage=75.0", \\\n "-XX:+ExitOnOutOfMemoryError", \\\n "-jar", "app.jar"]\n\nImportant: avoid fixed values for -Xmx and -Xms in a Docker environment. They do not adapt to changes in container limits during replication or scaling.
3. Layer Optimization and Build Caching
\n\nEach 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\nCorrect Dockerfile structure:
\n- \n
- Install OS dependencies (apk add, apt-get). \n
- Copy build tool configuration files (pom.xml, build.gradle). \n
- Download dependencies (mvn dependency:go-offline). \n
- Copy source code. \n
- Build and copy the artifact. \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\nAlso consider using
Blogs
Book Recommendations
Read also