DE
Containerization
Devops core v1.0.0
Containerization
Overview
Containerization packages applications with their dependencies into lightweight, portable images that run consistently across environments. Docker multi-stage builds produce minimal production images, while Docker Compose provides local development environments that mirror production topology. In the full-lifecycle pipeline, @devops-engineer generates Dockerfiles and compose configurations during Phase 11 based on the architecture and tech stack defined earlier.
Key Concepts
Container Image Layers
┌─────────────────────────────────────┐
│ Application JAR / Build Output │ ← Changes often (top layer)
├─────────────────────────────────────┤
│ Application Dependencies │ ← Changes with dependency updates
├─────────────────────────────────────┤
│ Runtime (JRE 21 / Node 20) │ ← Changes rarely
├─────────────────────────────────────┤
│ Base OS (Alpine / Distroless) │ ← Changes very rarely
└─────────────────────────────────────┘
↑ Arrange layers: least-changing at bottom
Base Image Selection
| Base Image | Size | Security | Use Case |
|---|---|---|---|
eclipse-temurin:21-jre-alpine | ~80MB | Good | Java production |
gcr.io/distroless/java21 | ~100MB | Best (no shell) | High-security Java |
node:20-alpine | ~50MB | Good | Node.js production |
nginx:alpine | ~25MB | Good | Static frontend |
ubuntu:24.04 | ~75MB | Low (full OS) | Avoid for production |
Best Practices
- Use multi-stage builds — Separate build and runtime stages; don’t ship compilers
- Use Alpine or Distroless — Minimal base images reduce attack surface
- Order layers by change frequency — Dependencies before source code
- Use .dockerignore — Exclude
.git,node_modules,target, IDE files - Run as non-root —
USER 1001:1001for security - Set health checks —
HEALTHCHECK CMD curl -f http://localhost:8080/actuator/health - Pin base image versions —
FROM node:20.11-alpine, notFROM node:latest - Scan images —
docker scoutortrivy imagein CI pipeline
Code Examples
✅ Good: Multi-Stage Java Dockerfile
# Stage 1: Build
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /build
# Cache dependencies (changes less often than source)
COPY pom.xml mvnw ./
COPY .mvn .mvn
RUN ./mvnw dependency:go-offline -B
# Build application
COPY src src
RUN ./mvnw package -DskipTests -B
RUN java -Djarmode=layertools -jar target/*.jar extract --destination extracted
# Stage 2: Runtime
FROM eclipse-temurin:21-jre-alpine AS runtime
# Security: run as non-root
RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -D appuser
WORKDIR /app
# Copy layers (ordered by change frequency)
COPY --from=builder /build/extracted/dependencies/ ./
COPY --from=builder /build/extracted/spring-boot-loader/ ./
COPY --from=builder /build/extracted/snapshot-dependencies/ ./
COPY --from=builder /build/extracted/application/ ./
# Security and configuration
USER 1001:1001
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
✅ Good: React Frontend Dockerfile
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
COPY . .
RUN npm run build
# Stage 2: Serve
FROM nginx:1.25-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
USER nginx
EXPOSE 80
HEALTHCHECK CMD wget --spider -q http://localhost:80/health || exit 1
✅ Good: Docker Compose for Local Development
# docker-compose.yml
version: '3.9'
services:
app:
build:
context: .
dockerfile: Dockerfile
target: builder # Use build stage for hot-reload
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: local
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/appdb
SPRING_DATASOURCE_USERNAME: app
SPRING_DATASOURCE_PASSWORD: app
SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092
depends_on:
postgres:
condition: service_healthy
volumes:
- ./src:/build/src # Hot reload source
postgres:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_DB: appdb
POSTGRES_USER: app
POSTGRES_PASSWORD: app
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
kafka:
image: confluentinc/cp-kafka:7.6.0
ports:
- "9092:9092"
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:29093
KAFKA_LISTENERS: PLAINTEXT://kafka:9092,CONTROLLER://kafka:29093
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
volumes:
postgres_data:
❌ Bad: Dockerfile Anti-Patterns
FROM ubuntu:latest # Untagged, huge base image
RUN apt-get update && apt-get install -y openjdk-21-jdk maven # Full JDK in runtime
COPY . /app # Copies everything (no .dockerignore)
WORKDIR /app
RUN mvn package # No dependency caching
EXPOSE 8080
CMD ["java", "-jar", "target/app.jar"] # Running as root
Anti-Patterns
- Single-stage builds — Ships JDK, Maven, source code in production image
- Running as root — Container compromise = host compromise
- No health checks — Orchestrator can’t detect unhealthy containers
- Using
latesttag — Non-reproducible builds; breaks without warning - No .dockerignore — Context includes
.git,node_modules, bloating image - Fat base images — Ubuntu/Debian when Alpine suffices
Testing Strategies
- Image scanning —
trivy image myapp:latestin CI - Container structure tests — Google’s
container-structure-testfor file/metadata assertions - Docker Compose integration —
docker compose up+ health check + test + teardown - Image size tracking — Alert if image exceeds size threshold