> ## 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.

# Containerization

> Dockerizing Spring Boot Applications

# 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!

```bash theme={null}
./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.

```dockerfile theme={null}
# 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`:

```yaml theme={null}
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`:

```xml theme={null}
<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

<AccordionGroup>
  <Accordion title="Why does the Dockerfile example use a multi-stage build with separate build and run stages? What are the security and performance implications of not doing this?">
    **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).
  </Accordion>

  <Accordion title="How do you properly configure JVM memory settings when running a Spring Boot application inside a Docker container?">
    **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.
  </Accordion>

  <Accordion title="Your team uses Docker Compose for local development with 6 services, PostgreSQL, Redis, Kafka, and Zipkin. Startup takes 3 minutes and developers complain. How do you optimize the development experience?">
    **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).
  </Accordion>
</AccordionGroup>
