Docker & Containers¶
Why containers¶
Package an app + its dependencies into a portable, isolated unit that runs the same everywhere. Lighter than VMs (share the host kernel; no full guest OS).
Containers vs VMs¶
| Container | VM | |
|---|---|---|
| Isolation | process-level (namespaces/cgroups) | full OS (hypervisor) |
| Overhead | low, fast start | heavy, slow start |
| Size | MBs | GBs |
Core concepts¶
- Image — immutable template (layers); built from a Dockerfile.
- Container — a running instance of an image.
- Registry — stores images (Docker Hub, ECR, Nexus).
- Volume — persistent storage outside the container's writable layer.
- Network — bridge/host/overlay for container communication.
Dockerfile essentials¶
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]
Common commands¶
docker build -t app . · docker run -p 8080:8080 app · docker ps · docker logs · docker exec -it <id> sh · docker compose up.
Docker Compose¶
Declare multi-container apps (app + db + cache) in docker-compose.yml for local dev/integration.
Java-Specific Containerization¶
Dockerfile for Java Application¶
Simple Approach (Single-stage)¶
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/myapp.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Problems: - Large image size (~400MB with full JRE) - Includes unnecessary build tools - Not optimized for production
Better: Multi-Stage Build¶
# Stage 1: Build
FROM eclipse-temurin:21 AS builder
WORKDIR /build
COPY pom.xml .
COPY src src
RUN mvn clean package -DskipTests
# Stage 2: Runtime (lightweight)
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Benefits: - Small final image (~150MB instead of 400MB) - Build artifacts not included - Faster layer caching
Best: Distroless + JVM Tuning¶
# Multi-stage with JVM optimization
FROM eclipse-temurin:21 AS builder
WORKDIR /build
COPY . .
RUN mvn clean package -DskipTests
# Use distroless for minimal image
FROM gcr.io/distroless/java21-debian11
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
# JVM flags for containers
ENV JAVA_TOOL_OPTIONS="-XX:+UseG1GC -XX:MaxRAMPercentage=75.0"
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
distroless advantages:
- Only Java runtime + app (no shell, no package managers)
- Image size: ~50-80MB
- Smaller attack surface
- Cannot docker exec into container (no shell)
JVM Flags for Containers¶
# Recognize container memory limits (Java 10+)
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport"
# Set heap automatically
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
# If container has 1GB RAM, heap = 750MB
# Use G1GC for consistent latency
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:+UseG1GC -XX:MaxGCPauseMillis=100"
# Virtual threads (Java 21+)
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport --enable-preview"
Why container-aware:
OLD: java -Xmx2G (fixed heap, ignores container limits)
NEW: java -XX:MaxRAMPercentage=75.0 (uses container's memory)
Layer Caching Optimization¶
# ❌ Bad: Changes to source invalidate all layers below
FROM eclipse-temurin:21
WORKDIR /app
COPY . . # Copies everything
RUN mvn clean package -DskipTests
# ✅ Better: Dependencies cached, only app code rebuilt
FROM eclipse-temurin:21
WORKDIR /app
COPY pom.xml pom.xml # Layer 1: Dependencies (stable)
RUN mvn dependency:resolve # Layer 2: Download deps (cached)
COPY src src # Layer 3: Source code (changes frequently)
RUN mvn clean package -DskipTests # Layer 4: Build (rebuilt if src changes)
Cache busting for frequent builds:
# Add timestamp to force rebuild
ARG BUILD_DATE
FROM eclipse-temurin:21
LABEL build_date=${BUILD_DATE}
...
Running Java in Kubernetes¶
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
spec:
replicas: 3
template:
spec:
containers:
- name: api
image: myrepo/api-service:1.0
ports:
- containerPort: 8080
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
Building & Pushing Images¶
# Build image
docker build -t myrepo/api-service:1.0 .
# Run locally
docker run -p 8080:8080 -e JAVA_TOOL_OPTIONS="-Xmx512m" myrepo/api-service:1.0
# Push to registry
docker push myrepo/api-service:1.0
# Kubernetes deployment
kubectl apply -f deployment.yaml
kubectl rollout status deployment/api-service
Docker Compose: Spring Boot + MySQL¶
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/mydb
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=secret
depends_on:
db:
condition: service_healthy
networks:
- app-network
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=secret
- MYSQL_DATABASE=mydb
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 5s
retries: 5
networks:
- app-network
volumes:
db-data:
networks:
app-network:
driver: bridge
Run locally:
docker-compose up # Starts app + db
docker-compose logs -f app # View logs
docker-compose down # Cleanup
Container Security Best Practices¶
# Run as non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
# Use read-only root filesystem where possible
SECURITY_OPTIONS='--read-only'
# Scan image for vulnerabilities
# docker scan myimage:latest
# Use specific base image tags (not 'latest')
FROM eclipse-temurin:21.0.1-jre-alpine # ✅ Specific version
FROM eclipse-temurin:21-jre # ❌ Floating tag
# Pin dependencies
RUN apt-get update && apt-get install -y \
specific-lib=1.2.3 \ # ✅ Pin version
another-lib # ❌ Floating version
Troubleshooting Container Issues¶
# Check container logs
docker logs <container-id>
# View container processes
docker exec <container-id> ps aux
# Check memory/CPU usage
docker stats
# Inspect environment variables
docker exec <container-id> env
# Test JVM settings
docker run --rm openjdk:21-jre java -XshowSettings:vm
# View image layers
docker history myimage:1.0
Image Size Comparison¶
Dockerfile approach | Image Size
─────────────────────────────|───────────
FROM openjdk:21 (full JDK) | 900MB ❌
FROM openjdk:21-jre | 400MB
FROM alpine + openjdk:21-jre | 150MB
FROM distroless/java21 | 50-80MB ✅
Best practice: Use distroless + multi-stage for production.