Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Dockerizing Spring Boot

Shipping JARs is so 2010. We ship Images.

1. The Easy Way: Cloud Native Buildpacks

Spring Boot has built-in Docker support. No Dockerfile needed!
./mvnw spring-boot:build-image
This creates an image named demo:0.0.1-SNAPSHOT using optimized layed JARs.

2. The Manual Way: Dockerfile

If you need custom control.
# Stage 1: Build
FROM eclipse-temurin:17-jdk-alpine as build
WORKDIR /app
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src
RUN ./mvnw install -DskipTests

# Stage 2: Run
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
Build it: docker build -t my-app .

3. Docker Compose for Development

Spin up your whole stack: Postgres, RabbitMQ, Zipkin, and your Apps. docker-compose.yml:
version: '3.8'
services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"

  rabbitmq:
    image: rabbitmq:management
    ports:
      - "5672:5672"
      - "15672:15672"

  zipkin:
    image: openzipkin/zipkin
    ports:
      - "9411:9411"

  user-service:
    build: ./user-service
    ports:
      - "8081:8080"
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/userdb
    depends_on:
      - postgres
      - rabbitmq
Run: docker-compose up -d

4. Jib (Google’s Plugin)

Builds optimized Docker images without a Docker daemon! Great for CI/CD pipelines. Add plugin to pom.xml:
<plugin>
    <groupId>com.google.cloud.tools</groupId>
    <artifactId>jib-maven-plugin</artifactId>
    <version>3.3.1</version>
</plugin>
Run: ./mvnw jib:dockerBuild

Interview Deep-Dive

Strong Answer:
  • A multi-stage build separates the build environment (JDK, Maven, source code, build tools) from the runtime environment (JRE, compiled JAR). The final image contains ONLY what is needed to run the application.
  • Without multi-stage: the image includes the full JDK (300MB+ vs. JRE’s 150MB), your entire source code (potentially including credentials if .gitignore is misconfigured), Maven’s local repository cache (hundreds of MB of downloaded dependencies), and build tools. This bloats the image from ~200MB to 800MB+.
  • Security implication: the JDK includes javac, jdb, jconsole, and other diagnostic tools. If an attacker gains shell access to the container, these tools make exploitation significantly easier. The JRE-only runtime reduces the attack surface. Even better, use a distroless base image (gcr.io/distroless/java17-debian12) — no shell, no package manager, no utilities. An attacker who gains code execution cannot run ls, curl, or bash.
  • Performance implication: smaller images mean faster pulls. In Kubernetes, when a pod is scheduled on a new node, it must download the image. An 800MB image takes 30+ seconds on a typical network; a 200MB image takes 8 seconds. This directly impacts your deployment speed and auto-scaling responsiveness.
  • Layer caching: the Dockerfile is ordered deliberately. Dependencies (pom.xml) are copied and resolved BEFORE source code. Since dependencies change less frequently than source code, Docker caches the dependency layer. On subsequent builds, only the source code layer is rebuilt, reducing build time from minutes to seconds.
Follow-up: What is the difference between the Spring Boot Buildpacks approach and a manual Dockerfile? When would you prefer each?Buildpacks (via ./mvnw spring-boot:build-image) automatically create an optimized layered image without a Dockerfile. They use Paketo buildpacks that detect your app type, select the appropriate JRE, configure JVM memory settings based on container limits, and create a layered JAR (dependencies, Spring libraries, snapshot dependencies, application code in separate layers). Advantage: zero Dockerfile maintenance, best-practice defaults, automatic security patching when you rebuild (new JRE patches are pulled). Disadvantage: less control over the base image, slower build process, and opaque — harder to debug when something goes wrong. I prefer Buildpacks for standard Spring Boot services where you do not need custom OS packages. I prefer Dockerfiles when you need custom native libraries, specific base images mandated by security policy, or multi-service builds in a single Dockerfile (monorepo pattern).
Strong Answer:
  • The critical problem: older JVMs (pre-Java 10) do not respect container memory limits. A container with 512MB RAM runs on a host with 64GB. The JVM reads the HOST memory (64GB) and sets its default heap to 16GB (-Xmx = 1/4 of physical memory). The container is killed by the OOM killer immediately.
  • Modern JVMs (Java 10+): the JVM is container-aware by default (-XX:+UseContainerSupport, on by default). It reads the container’s cgroup memory limit and sizes the heap accordingly. With 512MB container memory, the JVM sets the max heap to ~256MB (a conservative percentage of container memory, leaving room for metaspace, thread stacks, native memory, and the container overhead).
  • Fine-tuning: use -XX:MaxRAMPercentage=75.0 instead of a fixed -Xmx. This tells the JVM to use 75% of the container’s memory for the heap, adapting to different container sizes without hardcoded values. The remaining 25% covers metaspace (~100MB for Spring Boot), thread stacks (1MB per thread, 200 threads = 200MB), native memory (NIO buffers, JNI), and OS overhead.
  • Common production mistake: setting -Xmx=512m on a 512MB container. The heap is 512MB, but the JVM also needs 150MB for metaspace, 200MB for threads, and 50MB for internal bookkeeping. Total: ~900MB. Container is killed. Always set the heap to 60-75% of the container limit.
  • Spring Boot Buildpacks handle this automatically: the Paketo Java buildpack includes a memory calculator that reads the container memory limit and computes optimal -Xmx, -Xms, metaspace, and thread stack values.
Follow-up: A Spring Boot container keeps getting OOM-killed even though your heap usage looks healthy in JVM metrics. What is consuming the extra memory?Off-heap (native) memory. The JVM heap is only part of total memory consumption. Major off-heap consumers: (1) Metaspace — stores class metadata. Spring Boot with many dependencies can use 100-200MB. Monitor with jvm.memory.used{area=nonheap}. (2) Thread stacks — each thread consumes 1MB by default (-Xss). A Tomcat app with 200 request threads + 50 async threads = 250MB just for stacks. (3) Direct ByteBuffers — used by NIO, Netty, and memory-mapped files. If you use WebClient (Netty) heavily, this can grow significantly. (4) JNI and native libraries (compression, TLS). Monitor total RSS with docker stats and compare against JVM metrics. The gap is your native memory. Reduce with: -XX:MaxMetaspaceSize=150m, -Xss512k (reduce thread stack if your code is not deeply recursive), and -XX:MaxDirectMemorySize for NIO buffer limits.
Strong Answer:
  • Problem diagnosis: 3 minutes likely means services are starting before their dependencies are ready (Kafka is not up when OrderService starts, causing connection failures and restart loops), Docker is rebuilding images from scratch each time, and services are competing for CPU during the startup burst.
  • Fix 1 — Dependency health checks: Replace depends_on (which only waits for container start, not readiness) with health-check-based dependencies. depends_on: postgres: condition: service_healthy with a health check like test: ["CMD-SHELL", "pg_isready -U user"]. This ensures PostgreSQL is actually ready to accept connections before dependent services start.
  • Fix 2 — Volume mount for code: Instead of rebuilding the Docker image for every code change, mount the source code as a volume and use Spring DevTools with spring-boot-devtools. Code changes trigger an automatic restart inside the container without rebuilding the image. Build time drops from 60 seconds to 2 seconds for live reload.
  • Fix 3 — Pre-built infrastructure: Keep infrastructure containers (PostgreSQL, Redis, Kafka, Zipkin) running permanently. Only restart application containers during development. Use docker-compose up -d postgres redis kafka zipkin once, and run your Spring Boot services locally (outside Docker) pointed at localhost:5432, localhost:6379, etc. This eliminates container overhead for the services you are actively developing.
  • Fix 4 — Docker layer caching: Structure Dockerfiles to copy pom.xml and resolve dependencies before copying source code. Dependencies are cached; only the source code layer rebuilds on changes.
  • Fix 5 — Docker Compose profiles: Define profiles for different workflows. docker compose --profile full up starts everything. docker compose --profile infra up starts only infrastructure. Most development only needs infra + the one service you are working on.
Follow-up: How do you handle the situation where the Docker Compose environment behaves differently from the production Kubernetes environment?Parity gaps are inevitable but manageable. Key differences to watch: (1) DNS resolution — Docker Compose uses service names (postgres), Kubernetes uses service DNS (postgres.namespace.svc.cluster.local). Use Spring config profiles to abstract this. (2) Resource limits — Docker Compose typically runs without memory/CPU limits; production has strict limits. Add resource constraints to Compose: deploy: resources: limits: memory: 512M. (3) Networking — Compose puts everything on one network; Kubernetes has NetworkPolicies. A service-to-service call that works in Compose might be blocked by a NetworkPolicy in production. (4) Secrets — Compose uses environment variables; production uses Kubernetes Secrets or Vault. Use Spring’s @ConfigurationProperties to abstract the source. The best investment: run integration tests in a CI environment that mirrors production as closely as possible (Kubernetes-in-Docker, or a staging namespace).