Awesome-omni-skill docker-deployment

Docker containerization and deployment for Java/Spring Boot applications. Multi-stage builds, docker-compose, health checks, and CI/CD with GitHub Actions.

install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/devops/docker-deployment" ~/.claude/skills/diegosouzapw-awesome-omni-skill-docker-deployment && rm -rf "$T"
manifest: skills/devops/docker-deployment/SKILL.md
source content

Docker Deployment for Java/Spring Boot Applications

When to Use This Skill

Use when:

  • Containerizing a Java or Spring Boot application for consistent deployment
  • Setting up local development environments with Docker Compose
  • Building CI/CD pipelines that produce Docker images
  • Deploying to container orchestration platforms (Kubernetes, ECS, Cloud Run)
  • Ensuring reproducible builds across development, staging, and production
  • Managing multi-service architectures locally (app + database + cache)

Do NOT use when:

  • Building serverless functions (use cloud-native packaging instead)
  • The application is a CLI tool or batch job that does not need containerization
  • Deploying to a traditional VM-based environment with no container runtime
  • The project is a library/SDK (publish to Maven Central, not a container registry)

Multi-Stage Dockerfile for Java

Always use multi-stage builds to separate the build environment from the runtime. This produces smaller, more secure images.

Standard Pattern

# Stage 1: Build
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY pom.xml .
COPY .mvn/ .mvn/
COPY mvnw .
RUN chmod +x mvnw && ./mvnw dependency:go-offline -B
COPY src/ src/
RUN ./mvnw package -DskipTests -B

# Stage 2: Runtime
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /app/target/*.jar app.jar
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]

Key Principles

  1. Use JRE for runtime, not JDK -- the JDK includes compilers and tools not needed at runtime
  2. Use Alpine-based images when possible for smaller footprint (~200MB vs ~400MB)
  3. Run as non-root user -- never run Java applications as root in containers
  4. Copy dependency resolution step first to leverage Docker layer caching
  5. Use
    .dockerignore
    to exclude unnecessary files from the build context

JVM Options for Containers

Modern JVMs are container-aware (Java 10+), but explicit tuning is still recommended.

Essential Flags

# Memory: use percentage-based limits so the JVM respects container memory limits
-XX:MaxRAMPercentage=75.0
-XX:InitialRAMPercentage=50.0
-XX:MinRAMPercentage=25.0

# GC selection
-XX:+UseG1GC                    # General-purpose, good for most workloads
-XX:+UseZGC                     # Low-latency, good for large heaps (Java 17+)
-XX:+UseShenandoahGC            # Alternative low-latency GC

# Container awareness (enabled by default in Java 10+, but explicit is safer)
-XX:+UseContainerSupport

# Diagnostics
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof
-XX:+ExitOnOutOfMemoryError

Memory Sizing Guidelines

Container Memory LimitMaxRAMPercentageReasoning
256 MB75%Leaves ~64 MB for OS, native memory, threads
512 MB75%Leaves ~128 MB for overhead
1 GB75%Standard for most microservices
2 GB+80%Larger containers can use a higher percentage

Common Pitfall: OOMKilled

If your container is killed by the OOM killer (exit code 137), the JVM heap is exceeding the container memory limit. Solutions:

  • Lower
    MaxRAMPercentage
    (e.g., from 75% to 60%)
  • Increase the container memory limit
  • Check for native memory leaks (Netty buffers, JNI, thread stacks)
  • Use
    -XX:NativeMemoryTracking=summary
    for diagnostics

Docker Compose for Local Development

Use Docker Compose to run your application alongside its dependencies.

Structure

project/
  docker-compose.yml          # Base configuration (production-like)
  docker-compose.dev.yml      # Dev overrides (debug, live reload)
  docker-compose.test.yml     # Test overrides (test DB, mocks)
  .env                        # Environment variables (not committed)
  .env.example                # Template for environment variables (committed)

Override Pattern

# Development
docker compose -f docker-compose.yml -f docker-compose.dev.yml up

# Testing
docker compose -f docker-compose.yml -f docker-compose.test.yml up

# Production (base only)
docker compose up -d

Service Dependencies and Health Checks

Always use

depends_on
with
condition: service_healthy
so your application starts only after its dependencies are ready:

services:
  app:
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

Health Checks

Spring Boot Actuator Health Endpoint

Add the Actuator dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Configure in

application.yml
:

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      show-details: when-authorized
      probes:
        enabled: true    # Enables /actuator/health/liveness and /readiness
  health:
    livenessstate:
      enabled: true
    readinessstate:
      enabled: true

Dockerfile Health Check

HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD wget -qO- http://localhost:8080/actuator/health/liveness || exit 1

Docker Compose Health Check

services:
  app:
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8080/actuator/health/liveness"]
      interval: 30s
      timeout: 3s
      start_period: 60s
      retries: 3

Note: Use

start_period
to give the JVM time to start. Spring Boot applications typically need 15-60 seconds to initialize.


Environment Variables and Secrets

Best Practices

  1. Never bake secrets into images -- use environment variables or mounted secrets
  2. Use
    .env
    files for local development
    -- never commit
    .env
    to version control
  3. Use Docker secrets or external secret managers in production (Vault, AWS Secrets Manager, etc.)

Spring Boot Environment Variable Binding

Spring Boot automatically maps environment variables to properties using relaxed binding:

# These are equivalent:
SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/myapp
spring.datasource.url=jdbc:postgresql://db:5432/myapp

Docker Compose env_file

services:
  app:
    env_file:
      - .env
    environment:
      # Override specific values or add non-secret config
      - SPRING_PROFILES_ACTIVE=docker
      - SERVER_PORT=8080

.env.example Template

# Database
POSTGRES_DB=myapp
POSTGRES_USER=myapp
POSTGRES_PASSWORD=changeme

# Application
SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/myapp
SPRING_DATASOURCE_USERNAME=myapp
SPRING_DATASOURCE_PASSWORD=changeme

# Redis
SPRING_DATA_REDIS_HOST=redis
SPRING_DATA_REDIS_PORT=6379

# JWT / Security
JWT_SECRET=your-256-bit-secret-here

.dockerignore

A proper

.dockerignore
reduces build context size and prevents sensitive files from being included:

# Build artifacts
target/
build/
*.jar
*.war
!target/*.jar

# IDE files
.idea/
*.iml
.vscode/
.project
.classpath
.settings/

# Version control
.git/
.gitignore

# Docker files (not needed in context)
docker-compose*.yml
Dockerfile*
.dockerignore

# Documentation
*.md
LICENSE
docs/

# Environment and secrets
.env
.env.*
!.env.example

# OS files
.DS_Store
Thumbs.db

# CI/CD
.github/
.gitlab-ci.yml
Jenkinsfile

# Test reports
surefire-reports/
failsafe-reports/

Image Optimization

Layer Caching Strategy

Order Dockerfile instructions from least to most frequently changing:

# 1. Base image (rarely changes)
FROM eclipse-temurin:21-jre-alpine

# 2. System dependencies (rarely changes)
RUN apk add --no-cache wget

# 3. Application user (rarely changes)
RUN addgroup -S app && adduser -S app -G app

# 4. Dependencies (changes when pom.xml changes)
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./

# 5. Application code (changes most frequently)
COPY --from=builder /app/application/ ./

Spring Boot Layered JAR

Spring Boot 2.3+ supports layered JARs that split the fat JAR into layers for better Docker caching:

# Enable layered JAR in pom.xml (Spring Boot 3.x -- enabled by default)
# Extract layers after building
java -Djarmode=layertools -jar app.jar extract --destination extracted

# Layers (from least to most frequently changing):
# 1. dependencies          - third-party libraries
# 2. spring-boot-loader    - Spring Boot loader classes
# 3. snapshot-dependencies  - SNAPSHOT dependencies
# 4. application           - your compiled code and resources

Image Size Comparison

Base ImageApproximate Size
eclipse-temurin:21-jdk~460 MB
eclipse-temurin:21-jre~280 MB
eclipse-temurin:21-jre-alpine~180 MB
Custom JRE (jlink)~100 MB

Custom JRE with jlink

For maximum optimization, create a custom JRE with only the modules your application needs:

FROM eclipse-temurin:21-jdk-alpine AS jre-builder
RUN jlink \
    --add-modules java.base,java.logging,java.sql,java.naming,java.management,java.instrument,java.desktop,java.security.jgss,jdk.unsupported \
    --strip-debug \
    --no-man-pages \
    --no-header-files \
    --compress=zip-6 \
    --output /custom-jre

GitHub Actions CI/CD

Pipeline Structure

Push to main
  --> Run tests
  --> Build Docker image
  --> Push to container registry (GHCR)
  --> (optional) Deploy to staging

Key Features

  • Layer caching with
    docker/build-push-action
    and GitHub Actions cache
  • Multi-platform builds with
    docker/setup-buildx-action
  • Semantic versioning for image tags
  • Security scanning with Trivy or Snyk

Image Tagging Strategy

tags: |
  ghcr.io/${{ github.repository }}:latest
  ghcr.io/${{ github.repository }}:${{ github.sha }}
  ghcr.io/${{ github.repository }}:${{ github.ref_name }}

For production, prefer immutable tags (

sha
or semver) over
latest
.


Container Registries

GitHub Container Registry (GHCR)

# Login
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin

# Tag and push
docker tag myapp:latest ghcr.io/OWNER/myapp:latest
docker push ghcr.io/OWNER/myapp:latest

Docker Hub

docker login -u USERNAME
docker tag myapp:latest USERNAME/myapp:latest
docker push USERNAME/myapp:latest

Amazon ECR

aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin ACCOUNT.dkr.ecr.us-east-1.amazonaws.com
docker tag myapp:latest ACCOUNT.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
docker push ACCOUNT.dkr.ecr.us-east-1.amazonaws.com/myapp:latest

Spring Boot Docker Best Practices

1. Use Layered JARs

See

examples/Dockerfile.layered
for the complete example. Layered JARs dramatically improve rebuild times because dependency layers are cached.

2. Graceful Shutdown

# application.yml
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

In the Dockerfile or entrypoint, ensure signals are forwarded to the JVM:

# Use exec form (not shell form) so PID 1 is the JVM
ENTRYPOINT ["java", "-jar", "app.jar"]

# Or use tini as init process
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--", "java", "-jar", "app.jar"]

3. Spring Profiles for Docker

# application-docker.yml
spring:
  datasource:
    url: jdbc:postgresql://${DB_HOST:postgres}:${DB_PORT:5432}/${DB_NAME:myapp}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
  data:
    redis:
      host: ${REDIS_HOST:redis}
      port: ${REDIS_PORT:6379}

server:
  port: ${SERVER_PORT:8080}

logging:
  pattern:
    console: "%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n"

4. Log to stdout/stderr

Never log to files inside containers. Use console appenders and let the container runtime collect logs:

logging:
  file:
    name:   # Explicitly empty -- no file logging
  level:
    root: INFO
    com.myapp: DEBUG

5. Externalize Configuration

Use environment variables or mounted config files. Never bake environment-specific configuration into the image.


Production Deployment Considerations

Resource Limits

Always set memory and CPU limits:

# docker-compose.yml
services:
  app:
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: "1.0"
        reservations:
          memory: 512M
          cpus: "0.5"

Logging

  • Use JSON-formatted logs for structured log aggregation
  • Ship logs to a centralized system (ELK, Loki, CloudWatch)
  • Set appropriate log levels (INFO for production, DEBUG for troubleshooting)

Security

  • Scan images for vulnerabilities (
    docker scout
    ,
    trivy
    ,
    snyk
    )
  • Use read-only file systems where possible (
    read_only: true
    )
  • Drop all capabilities and add only what is needed
  • Pin base image versions with digest hashes for reproducibility
  • Never store secrets in image layers

Networking

  • Use Docker networks to isolate services
  • Only expose ports that need to be publicly accessible
  • Use internal DNS names for service-to-service communication

Monitoring

  • Expose Prometheus metrics via Actuator (
    /actuator/prometheus
    )
  • Use liveness and readiness probes
  • Monitor container resource usage (memory, CPU, restarts)
  • Set up alerts for container OOM kills and restart loops

Code Quality Checklist

Before deploying a containerized Java application, verify:

  • Multi-stage build: Build and runtime stages are separate
  • Non-root user: Application runs as a non-root user
  • Health check: Dockerfile or Compose includes a health check
  • JVM tuning:
    MaxRAMPercentage
    is set appropriately
  • Container memory limit:
    deploy.resources.limits.memory
    is configured
  • No secrets in image: All secrets come from environment variables or secret managers
  • .dockerignore: Build context excludes unnecessary files
  • Graceful shutdown: Spring Boot
    server.shutdown=graceful
    is enabled
  • Log to stdout: No file-based logging inside the container
  • Image tagged: Using immutable tags (SHA or semver), not just
    latest
  • Vulnerability scan: Image has been scanned for known CVEs
  • Layered JAR: Spring Boot layered JAR is used for optimal caching
  • Base image pinned: Using a specific version tag, not
    latest
  • Entrypoint uses exec form: Ensures proper signal handling
  • CI/CD pipeline: Automated build, test, scan, and push workflow exists