Docker and Java: Advanced Application Containerization
Containerizing Java applications with Docker has long become an industry standard. However, basic Dockerfiles and simple docker run commands are just the tip of the iceberg. At an advanced level, developers face challenges such as optimizing image sizes, fine-tuning the JVM inside a container, organizing multi-stage builds, and orchestrating microservices. In this article, we will cover key techniques that will help you take Java containerization to a professional level.
1. Multi-Stage Builds: Minimal Image for Production
One of the main problems when containerizing Java is the size of the final image. The base openjdk:17-jdk image weighs about 400 MB, which is excessive for runtime. The solution is a multi-stage build.
The first stage (build) uses the full JDK and build tools (Maven/Gradle). The second stage (runtime) uses a lightweight JRE image and copies only the built JAR/WAR.
# Stage 1: Build the applicationFROM maven:3.8.5-openjdk-17 AS builderWORKDIR /appCOPY pom.xml .COPY src ./srcRUN mvn clean package -DskipTests
# Stage 2: Minimal runtime imageFROM openjdk:17-jre-slimWORKDIR /app# Copy only the built artifactCOPY --from=builder /app/target/myapp-*.jar app.jarEXPOSE 8080CMD ["java", "-jar", "app.jar"]Advantages:
- The final image weighs ~180 MB instead of 400+ MB
- Reduces the attack surface (no compiler, unnecessary utilities)
- Speeds up deployment and image download times
2. Configuring JVM for Container Operation
For a long time, the JVM adapted poorly to container constraints (cgroups). Starting with Java 10, flags for explicitly specifying resources appeared, and in Java 17+ the behavior became more predictable. However, at an advanced level, it is important to manually control memory and CPU.
Key JVM parameters for containers:
-XX:MaxRAMPercentage=75.0— allocates 75% of the container's memory for the heap (instead of fixed -Xmx)-XX:+UseContainerSupport— enables awareness of container limits (enabled by default since Java 10+)-XX:ActiveProcessorCount=2— explicitly specifies the number of available processors-XX:+ExitOnOutOfMemoryError— terminates the process on OutOfMemoryError (for fast container restart)
# Example Dockerfile with optimized JVM flagsFROM eclipse-temurin:17-jre-alpineWORKDIR /appCOPY target/app.jar app.jar
# JVM configuration for the containerENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \ -XX:+UseContainerSupport \ -XX:+ExitOnOutOfMemoryError \ -XX:+PrintGCDetails"
EXPOSE 8080CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]Important: Use eclipse-temurin or amazon-corretto — these are official, well-supported images with a transparent update policy.
3. Docker Compose for Microservice Architecture
In real projects, a Java application rarely exists in isolation. It is usually a combination: Spring Boot backend + PostgreSQL + Redis + Kafka. Docker Compose allows you to describe the entire infrastructure in a single YAML file.
version: '3.8'
services: postgres: image: postgres:15-alpine environment: POSTGRES_DB: myapp POSTGRES_USER: user POSTGRES_PASSWORD: secret volumes: - pgdata:/var/lib/postgresql/data networks: - backend
redis: image: redis:7-alpine ports: - "6379:6379" networks: - backend
app: build: context: . dockerfile: Dockerfile ports: - "8080:8080" environment: SPRING_DATASOURCE_URL: jdbc:postgresql: