AgentSkillsCN

docker-deployment

针对 Java/Spring Boot 应用程序的 Docker 容器化与部署。采用多阶段构建、docker-compose、健康检查,并结合 GitHub Actions 实现 CI/CD。

SKILL.md
--- frontmatter
name: docker-deployment
description: Docker containerization and deployment for Java/Spring Boot applications. Multi-stage builds, docker-compose, health checks, and CI/CD with GitHub Actions.
version: "1.0.0"
author: GazApps
tags: [docker, deployment, container, docker-compose, ci-cd, github-actions, devops]
dependencies: [java-fundamentals]
compatibility: [antigravity, claude-code, gemini-cli]

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

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

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

code
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

bash
# 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:

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

Health Checks

Spring Boot Actuator Health Endpoint

Add the Actuator dependency:

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

Configure in application.yml:

yaml
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

dockerfile
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

yaml
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:

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

Docker Compose env_file

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

.env.example Template

bash
# 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:

code
# 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:

dockerfile
# 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:

bash
# 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:

dockerfile
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

code
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

yaml
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)

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

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

Amazon ECR

bash
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

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

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

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

yaml
# 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:

yaml
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:

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