Java Fundamentals Tutorial 0/33 lessons ~6 min read Lesson 28

    Docker

    Docker packages Java applications as immutable images — filesystem snapshots with JRE, jar, and config — run as isolated containers on any host.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Docker packages Java applications as immutable images — filesystem snapshots with JRE, jar, and config — run as isolated containers on any host. Multi-stage builds compile with JDK in one stage and ship slim JRE-only runtime in another, cutting image size from 800MB to 150MB.

    JVM in containers requires explicit tuning: -XX:MaxRAMPercentage, container-aware defaults since Java 10+, and awareness that Docker memory limits ≠ host memory. This lesson is how staff engineers ship Temurin 21 Spring Boot services that start fast, fit CI/CD, and behave predictably under cgroup limits.

    Business problem

    "Works on my machine" without containers:

    • Environment drift: Dev runs Java 17, prod runs Java 11 — subtle bytecode/runtime bugs.
    • Deploy friction: SSH + manual jar copy — 45-minute deploy, no rollback artifact.
    • Resource conflict: Two services on same VM fight for heap — OOM kills wrong process.
    • Slow CI: 900MB fat JDK image pulled every build — pipeline queue grows.
    • Security surface: Full JDK in production — javac, javadoc attack surface unnecessary.

    Why this topic exists

    Docker standardizes the unit of deployment — image digest is the contract:

    • Image: Immutable layers — base OS + JRE + application jar + config.
    • Container: Running instance of image — isolated PID, network, filesystem (copy-on-write).
    • Multi-stage build: Stage 1 compile with JDK; stage 2 copy only jar + JRE — smaller, safer.
    • JVM container flags: -XX:+UseContainerSupport (default Java 10+) — heap respects Docker memory limit.
    • Orchestration handoff: Same image runs locally, CI, Kubernetes — parity guaranteed.

    Core concepts

    Docker + JVM core concepts:

    • Dockerfile: Declarative image recipe — FROM, COPY, RUN, ENTRYPOINT, EXPOSE.
    • Layer caching: Order Dockerfile for cache hits — dependency copy before source copy in builds.
    • Distroless/JRE-alpine: Minimal base — eclipse-temurin:21-jre-alpine vs full ubuntu.
    • ENTRYPOINT exec form: ["java", "-jar", "/app.jar"] — PID 1 receives SIGTERM for graceful shutdown.
    • MaxRAMPercentage: -XX:MaxRAMPercentage=75.0 — heap = 75% of container memory limit.
    • Spring Boot layertools: Extract layers (dependencies, snapshot, application) — Docker layer cache for deps.

    Internal architecture

    Multi-stage Java Docker build:

    text
    # Stage 1 — build (discarded from final image)
    FROM eclipse-temurin:21-jdk-alpine AS builder
    WORKDIR /app
    COPY gradlew settings.gradle.kts build.gradle.kts ./
    COPY gradle ./gradle
    RUN ./gradlew dependencies --no-daemon # cached layer
    COPY src ./src
    RUN ./gradlew bootJar --no-daemon -x test
    # Stage 2 — runtime (what ships to prod)
    FROM eclipse-temurin:21-jre-alpine
    RUN addgroup -S app && adduser -S app -G app
    WORKDIR /app
    COPY --from=builder /app/build/libs/*.jar app.jar
    USER app
    EXPOSE 8080
    ENTRYPOINT ["java", \
    "-XX:+UseContainerSupport", \
    "-XX:MaxRAMPercentage=75.0", \
    "-XX:+UseZGC", \
    "-jar", "/app.jar"]
    # Image size: ~180MB (JRE-alpine + jar) vs ~900MB (JDK + build tools)

    Image layers, container runtime, and JVM memory in cgroups:

    Multi-stage build flow
    JDK stage
    Compile bootJar
    Discard
    No JDK in prod
    JRE stage
    Copy jar only
    Push digest
    Immutable tag
    Final image contains only runtime — attack surface and pull size minimized.
    Container isolation
    Image layers
    Read-only FS
    Container
    Writable layer
    cgroup limit
    512Mi memory
    JVM heap
    75% of limit
    JVM reads cgroup memory — MaxRAMPercentage sets heap cap inside limit.
    Spring Boot layer caching
    deps layer
    Rarely changes
    snapshot layer
    Occasional
    app layer
    Every commit
    Fast rebuild
    Cache hit deps
    Layertools EXTRACT + separate COPY layers — CI rebuild seconds not minutes.

    Code walkthrough

    Production Dockerfile with Spring Boot layertools:

    • layertools extract: Splits jar into cache-friendly Docker layers — dependencies change rarely.
    • JarLauncher: Spring Boot loader — same as java -jar but from extracted dirs.
    • memory: 512M: Docker limit — JVM sets max heap ~384MB with MaxRAMPercentage=75.
    • service_healthy: Wait for Postgres ready — avoid crash-loop on startup race.
    java
    # Dockerfile with layertools (Spring Boot 2.3+)
    FROM eclipse-temurin:21-jdk-alpine AS builder
    WORKDIR /build
    COPY build/libs/application.jar app.jar
    RUN java -Djarmode=layertools -jar app.jar extract
    FROM eclipse-temurin:21-jre-alpine
    WORKDIR /app
    COPY --from=builder /build/dependencies/ ./
    COPY --from=builder /build/spring-boot-loader/ ./
    COPY --from=builder /build/snapshot-dependencies/ ./
    COPY --from=builder /build/application/ ./
    ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:+UseZGC"
    ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]
    # docker-compose.yml — local stack
    services:
    payment-api:
    build: .
    ports: ["8080:8080"]
    environment:
    SPRING_PROFILES_ACTIVE: docker
    SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/payments
    deploy:
    resources:
    limits:
    memory: 512M
    depends_on:
    db:
    condition: service_healthy

    Production example

    JVM flags for containerized Java 21 services:

    • ExitOnOutOfMemoryError: Kill container on OOM — K8s restarts fresh instance, alerts fire.
    • InitialRAMPercentage: Faster startup — don't wait to grow heap to max.
    • .dockerignore: Exclude .git and build cache — send only jar to daemon.
    • HEALTHCHECK: Docker-native health; K8s uses separate probes but useful in compose.
    bash
    # Recommended production ENTRYPOINT
    ENTRYPOINT ["java", \
    "-XX:+UseContainerSupport", \
    "-XX:MaxRAMPercentage=75.0", \
    "-XX:InitialRAMPercentage=50.0", \
    "-XX:+UseZGC", \
    "-XX:+ExitOnOutOfMemoryError", \
    "-Djava.security.egd=file:/dev/./urandom", \
    "-jar", "/app.jar"]
    # .dockerignore — faster builds, smaller context
    .git
    .gradle
    build
    !build/libs/*.jar
    *.md
    target
    # Health check in Dockerfile
    HEALTHCHECK --interval=30s --timeout=3s --start-period=60s \
    CMD wget -qO- http://localhost:8080/actuator/health || exit 1
    # Build and scan
    docker build -t payment-api:1.2.3 .
    docker scout cves payment-api:1.2.3 # or trivy image payment-api:1.2.3

    Enterprise case study

    Spotify — Docker adoption: Spotify containerized Java services early — immutable artifacts from CI, identical local and prod. Key lesson: never run as root in container — add non-root USER; K8s PodSecurityStandards enforce this. Second lesson: pin base image digest, not floating :latest tag — reproducible builds and CVE tracking.

    • Before: Pet servers, snowflake configs, manual JVM tuning per host.
    • After: Image digest promoted through dev → staging → prod; rollback = previous digest.
    • JVM win: Container-aware heap — eliminated OOM from JVM assuming host RAM (32GB heap in 512MB container).
    • CI win: Layertools — dependency layer cache hit 90% of builds.

    Performance considerations

    Java container performance:

    • Image pull time: Slim JRE-alpine — faster K8s pod schedule on new nodes.
    • Startup time: CDS (Class Data Sharing) — -XX:SharedArchiveFile=app.jsa for faster boot.
    • CPU limits: JVM sees cgroup CPU quota — GC threads adjust; don't set CPU limit too tight on latency services.
    • ZGC in containers: Low pause GC pairs well with tight memory limits and K8s liveness probes.
    • Build cache: Multi-stage + layertools — CI pipeline under 2 minutes for incremental builds.

    Security considerations

    Container security for Java:

    • Non-root USER: Run as app user — container escape impact reduced.
    • No secrets in image: DB passwords via env/Secrets at runtime — never COPY .env into image.
    • Minimal base: JRE-alpine or distroless — no shell in prod image if possible.
    • Scan images: Trivy/Scout in CI — fail build on critical CVE in base image.
    • Read-only root FS: K8s securityContext.readOnlyRootFilesystem — tmp volume for logs if needed.

    Scalability considerations

    Docker in scaled deployments:

    • Immutable scale: Same image, N containers — horizontal scale unit.
    • Registry: ECR/GCR/Artifactory — geo-replicated for fast pulls globally.
    • Build once, deploy many: CI builds image once; promote digest — no rebuild per environment.
    • Resource requests: Set memory limit = JVM MaxRAMPercentage headroom + metaspace + native + 25% buffer.

    Production challenges

    Real Docker + Java pain points:

    • OOMKilled: Container memory limit too low for heap + metaspace + direct buffers — increase limit or lower MaxRAMPercentage.
    • Slow startup: Spring context + fat jar — layertools, CDS, reduce classpath scanning.
    • PID 1 signal handling: Shell form ENTRYPOINT ignores SIGTERM — use exec form for graceful shutdown.
    • Clock/timezone: Container UTC default — set TZ env or -Duser.timezone for logging consistency.
    • File permissions: Volume mounts as root — app user cannot write — initContainer or fsGroup fix.

    Common mistakes

    • Running JDK in production image — bloated and unnecessary attack surface.
    • No MaxRAMPercentage — JVM allocates heap based on host RAM, container OOMKilled.
    • Shell ENTRYPOINT without exec — SIGTERM doesn't reach Java process — ungraceful kill.
    • COPY entire project before dependency resolve — Docker cache bust every code change.
    • Floating latest tag in prod — untraceable deploys, surprise breaking base image updates.

    Debugging guide

    Debug containerized Java:

    • Container OOM: dmesg | grep oom or K8s OOMKilled reason — tune memory limit and MaxRAMPercentage.
    • Exec into container: docker exec -it payment-api sh — check env, curl localhost actuator.
    • JVM flags verify: docker run ... java -XX:+PrintFlagsFinal | grep RAM.
    • Layer analysis: dive payment-api:1.2.3 — find bloated layers.
    bash
    # Inspect container memory limit vs JVM heap
    docker stats payment-api
    # Logs
    docker logs -f --tail 100 payment-api
    # Run with shell override for debug (dev only)
    docker run -it --entrypoint sh payment-api:1.2.3
    # Check image size and layers
    docker history payment-api:1.2.3 --human

    Best practices

    • Use multi-stage builds — JDK build, JRE runtime only.
    • Set -XX:MaxRAMPercentage=75 — heap fits container memory limit.
    • ENTRYPOINT exec form — proper SIGTERM for graceful Spring shutdown.
    • Run as non-root USER — security baseline.
    • Use Spring Boot layertools — cache dependency layers in CI.
    • Pin base image digest — reproducible builds.
    • Add .dockerignore — fast builds, lean context.

    Anti-patterns

    • Single-stage fat image with Maven/Gradle inside production container.
    • Hardcoding config in Dockerfile — use env vars and external config.
    • Running docker build on production server — CI builds, prod pulls digest.
    • Ignoring HEALTHCHECK and actuator — deploy blind to broken startup.
    • java -Xmx512m with container limit 512M — no room for metaspace/native — OOMKilled.

    Staff engineer notes

    • Staff engineers treat Dockerfile as production code — reviewed in PR, versioned, tested in CI.
    • Container memory limit must include heap + non-heap + direct memory + 20% headroom — not equal to -Xmx.
    • Distroless is ideal for security; alpine JRE acceptable when you need shell for debugging in staging.
    • Graceful shutdown: Spring handles SIGTERM if PID 1 is java — set terminationGracePeriodSeconds ≥ 30 in K8s.
    • Image promotion by digest, not tag — tag :1.2.3 points to sha256:abc... for audit trail.

    Interview questions

    Interview preparation

    15 questions grouped by difficulty — expand any card for a model answer and follow-up probe.

    Beginner

    5
    1. 1What is a Docker image vs container?
      Beginner

      Model answer

      Image: immutable read-only template with layers (OS, JRE, app).

      Container: running instance with writable layer on top.

      Many containers from one image.

      Image built by Dockerfile; container started by docker run.

      Follow-up probe

      Image layer caching?

    2. 2What is a multi-stage Docker build?
      Beginner

      Model answer

      Multiple FROM stages in one Dockerfile.

      Early stage compiles with JDK; final stage copies only artifact to slim JRE base.

      Intermediate stages discarded.

      Smaller, more secure production image.

      Follow-up probe

      COPY --from?

    3. 3How JVM behaves in Docker containers?
      Beginner

      Model answer

      • Java 10+ UseContainerSupport detects cgroup memory/CPU limits. MaxRAMPercentage sets max heap as percentage of container memory
      • not host RAM. Prevents OOM from JVM over-allocating inside limited container.

      Follow-up probe

      Default before Java 10?

    4. 4Why use JRE not JDK in production image?
      Beginner

      Model answer

      • JRE runs application only
      • smaller image, faster pull, reduced attack surface. JDK includes compiler, javadoc, tools not needed at runtime. Multi-stage build compiles with JDK, ships JRE.

      Follow-up probe

      jlink custom runtime?

    5. 5Explain ENTRYPOINT exec vs shell form.
      Beginner

      Model answer

      • Exec: ["java", "-jar", "app.jar"]
      • java is PID 1, receives SIGTERM. Shell: sh -c java ...
      • shell is PID 1, may not forward signals
      • graceful shutdown breaks. Always exec form for Java.

      Follow-up probe

      SIGTERM Spring Boot?

    Intermediate

    5
    1. 6What is -XX:MaxRAMPercentage?
      Intermediate

      Model answer

      Sets maximum heap as percentage of container memory limit detected via cgroups.

      g.

      75% of 512MB limit ≈ 384MB max heap.

      Leaves room for metaspace, thread stacks, direct buffers, native memory.

      Follow-up probe

      vs -Xmx?

    2. 7Spring Boot layertools in Docker?
      Intermediate

      Model answer

      • java -Djarmode=layertools -jar app.jar extract splits jar into dependencies, spring-boot-loader, snapshot-dependencies, application directories. COPY each as separate Docker layer
      • dependency layer cached when only app code changes.

      Follow-up probe

      Boot 3 alternative?

    3. 8Why container gets OOMKilled despite -Xmx?
      Intermediate

      Model answer

      Container limit must cover heap + metaspace + code cache + direct memory + native + JVM overhead.

      -Xmx=512m in 512m container leaves no headroom.

      5× intended heap.

      Follow-up probe

      Native memory leak?

    4. 9Docker compose vs Kubernetes?
      Intermediate

      Model answer

      • Compose: multi-container local dev, single host, simple networking. Kubernetes: production orchestration
      • scheduling, scaling, service discovery, self-healing across cluster. Compose for dev; K8s for prod at scale.

      Follow-up probe

      Same image both?

    5. 10How secure Java Docker images?
      Intermediate

      Model answer

      • Non-root user, minimal base (alpine/distroless), no secrets in layers, scan CVEs in CI, pin digest, read-only root FS, no SSH in container. Distroless removes shell
      • reduces attack surface.

      Follow-up probe

      Secret in env?

    Advanced

    5
    1. 11Design CI/CD for Spring Boot Docker.
      Advanced

      Model answer

      Gradle bootJar → multi-stage Dockerfile with layertools → build in CI → scan Trivy → push to ECR with semver+digest → deploy to K8s by digest.

      Cache dependency layer.

      Integration test with testcontainers.

      Rollback previous digest.

      Follow-up probe

      Build cache in GitHub Actions?

    2. 12Optimize Java container startup time.
      Advanced

      Model answer

      Spring lazy-init where safe, CDS archive, slim classpath (exclude unused auto-config), layertools, adequate CPU for JIT during warmup, readiness probe startPeriod 60s.

      Consider GraalVM native-image for cold start critical (trade JIT peak throughput).

      Follow-up probe

      CDS how?

    3. 13Docker networking for microservices?
      Advanced

      Model answer

      Compose: service name DNS (payment-api:8080).

      Bridge network isolates stack.

      K8s: ClusterIP Service DNS.

      Don't use host networking unless performance critical.

      Expose only through gateway/ingress.

      Follow-up probe

      Container-to-container?

    4. 14When NOT to containerize?
      Advanced

      Model answer

      Legacy app with hard host dependencies, license tied to hardware, extreme low-latency bare metal trading (sometimes).

      Most Java enterprise services benefit.

      Windows containers niche.

      Follow-up probe

      VM vs container?

    5. 15JVM flags checklist for K8s Java pod?
      Advanced

      Model answer

      UseContainerSupport (default), MaxRAMPercentage=75, InitialRAMPercentage=50, UseZGC or G1, ExitOnOutOfMemoryError, ActiveProcessorCount if CPU limit set.

      Match memory request/limit.

      Graceful shutdown on SIGTERM.

      Actuator health for probes.

      Follow-up probe

      CPU limit affects GC?

    Hands-on exercise

    Lab: Docker for Java:

    • Run playground — review simulated JVM memory calculation.
    • Write minimal 2-stage Dockerfile pseudocode in comments.
    • Calculate: 512MB limit, MaxRAMPercentage=75 → max heap?
    • List ENTRYPOINT exec form for Spring Boot jar.
    • Bonus: write .dockerignore entries for Gradle project.

    JavaDocker for Java Services

    Starter Templates
    OutputRemote JVM (Piston · Java 15)
    Press Run to execute. Real javac + JVM via remote API. Falls back to simulator on network failure.

    Architecture trade-offs

    • Alpine vs Debian JRE: Alpine wins size; Debian wins glibc compatibility (some JNI libs).
    • MaxRAMPercentage vs fixed -Xmx: Percentage wins portable across envs; fixed -Xmx wins explicit tuning.
    • Layertools vs fat jar COPY: Layertools wins CI cache; fat jar wins simplicity.
    • Distroless vs alpine: Distroless wins security; alpine wins debug shell access.

    Summary

    Docker is how Java services ship — multi-stage builds for slim images, container-aware JVM flags for correct heap, and layertools for fast CI. Master images vs containers and JVM cgroup behavior before Kubernetes scheduling adds another layer. Next: deploy these images as Pods with Deployments and Services.

    Key takeaways

    • Multi-stage build: compile with JDK, ship JRE-only runtime image.
    • MaxRAMPercentage — JVM heap must fit inside Docker memory limit.
    • ENTRYPOINT exec form — Java as PID 1 for graceful SIGTERM shutdown.
    • Spring layertools — cache dependency layers in CI.
    • Non-root user, scan images, pin digest — production hygiene.
    Ready to mark this lesson complete?Track your journey across the entire course.