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.
git clone https://github.com/diegosouzapw/awesome-omni-skill
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"
skills/devops/docker-deployment/SKILL.mdDocker 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
- Use JRE for runtime, not JDK -- the JDK includes compilers and tools not needed at runtime
- Use Alpine-based images when possible for smaller footprint (~200MB vs ~400MB)
- Run as non-root user -- never run Java applications as root in containers
- Copy dependency resolution step first to leverage Docker layer caching
- Use
to exclude unnecessary files from the build context.dockerignore
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 Limit | MaxRAMPercentage | Reasoning |
|---|---|---|
| 256 MB | 75% | Leaves ~64 MB for OS, native memory, threads |
| 512 MB | 75% | Leaves ~128 MB for overhead |
| 1 GB | 75% | 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
(e.g., from 75% to 60%)MaxRAMPercentage - Increase the container memory limit
- Check for native memory leaks (Netty buffers, JNI, thread stacks)
- Use
for diagnostics-XX:NativeMemoryTracking=summary
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
- Never bake secrets into images -- use environment variables or mounted secrets
- Use
files for local development -- never commit.env
to version control.env - 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 Image | Approximate 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
and GitHub Actions cachedocker/build-push-action - 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:
is set appropriatelyMaxRAMPercentage - Container memory limit:
is configureddeploy.resources.limits.memory - No secrets in image: All secrets come from environment variables or secret managers
- .dockerignore: Build context excludes unnecessary files
- Graceful shutdown: Spring Boot
is enabledserver.shutdown=graceful - 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