Containerization với Docker - Đóng gói Java Applications

Tổng quan về Docker

Docker là platform containerization cho phép đóng gói Java applications cùng với dependencies thành containers có thể chạy consistently trên mọi environment.

Docker Basics cho Java

Dockerfile cho Java Applications

Spring Boot Application

# Multi-stage build cho Spring Boot
FROM eclipse-temurin:17-jdk-alpine AS builder

# Set working directory
WORKDIR /app

# Copy Maven wrapper and pom.xml
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .

# Download dependencies (cached layer)
RUN ./mvnw dependency:resolve

# Copy source code
COPY src src

# Build application
RUN ./mvnw package -DskipTests

# Runtime stage
FROM eclipse-temurin:17-jre-alpine

# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init

# Create app user
RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup

# Set working directory
WORKDIR /app

# Copy jar from builder stage
COPY --from=builder /app/target/*.jar app.jar

# Change ownership
RUN chown -R appuser:appgroup /app

# Switch to non-root user
USER appuser

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8080/actuator/health || exit 1

# Expose port
EXPOSE 8080

# Set JVM options
ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+UseContainerSupport"

# Entry point with dumb-init
ENTRYPOINT ["dumb-init", "--"]
CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

Gradle-based Application

FROM eclipse-temurin:17-jdk-alpine AS builder

WORKDIR /app

# Copy Gradle wrapper
COPY gradlew .
COPY gradle gradle

# Copy build files
COPY build.gradle .
COPY settings.gradle .

# Download dependencies
RUN ./gradlew dependencies --no-daemon

# Copy source
COPY src src

# Build application
RUN ./gradlew bootJar --no-daemon

# Runtime stage
FROM eclipse-temurin:17-jre-alpine

RUN apk add --no-cache dumb-init curl

RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup

WORKDIR /app

COPY --from=builder /app/build/libs/*.jar app.jar

RUN chown -R appuser:appgroup /app

USER appuser

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8080/actuator/health || exit 1

EXPOSE 8080

ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC"

ENTRYPOINT ["dumb-init", "--"]
CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

.dockerignore

# Version control
.git
.gitignore

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

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

# OS files
.DS_Store
Thumbs.db

# Documentation
*.md
docs/

# Test files
src/test/

# Logs
*.log
logs/

# Temporary files
*.tmp
*.swp
*~

# Environment files
.env
.env.local

Docker Compose cho Development

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=dev
      - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/myapp
      - SPRING_DATASOURCE_USERNAME=root
      - SPRING_DATASOURCE_PASSWORD=password
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./logs:/app/logs
    networks:
      - app-network
    restart: unless-stopped

  db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: myapp
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - app-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      timeout: 20s
      retries: 10

volumes:
  mysql_data:

networks:
  app-network:
    driver: bridge

Production Docker Compose

# docker-compose.prod.yml
version: '3.8'

services:
  app:
    image: myapp:${VERSION:-latest}
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/myapp
      - SPRING_DATASOURCE_USERNAME_FILE=/run/secrets/db_username
      - SPRING_DATASOURCE_PASSWORD_FILE=/run/secrets/db_password
      - SPRING_REDIS_HOST=redis
      - JAVA_OPTS=-Xms1g -Xmx2g -XX:+UseG1GC -XX:+UseContainerSupport
    secrets:
      - db_username
      - db_password
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 30s
        failure_action: rollback
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
      resources:
        limits:
          memory: 2G
          cpus: '1.0'
        reservations:
          memory: 1G
          cpus: '0.5'
    networks:
      - app-network
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: myapp
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - app-network
    deploy:
      placement:
        constraints: [node.role == manager]
      resources:
        limits:
          memory: 1G
          cpus: '0.5'

secrets:
  db_username:
    external: true
  db_password:
    external: true

volumes:
  mysql_data:

networks:
  app-network:
    driver: overlay

Docker Build Optimization

Multi-stage Build Example

# Optimized multi-stage build
FROM maven:3.8.6-eclipse-temurin-17 AS dependencies

WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B

FROM maven:3.8.6-eclipse-temurin-17 AS builder

WORKDIR /app
COPY --from=dependencies /root/.m2 /root/.m2
COPY pom.xml .
COPY src src
RUN mvn package -DskipTests -B

FROM eclipse-temurin:17-jre-alpine AS runtime

# Security updates
RUN apk update && apk upgrade && \
    apk add --no-cache dumb-init curl && \
    rm -rf /var/cache/apk/*

# Non-root user
RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup

WORKDIR /app

# Copy only necessary files
COPY --from=builder /app/target/*.jar app.jar

# Set permissions
RUN chown -R appuser:appgroup /app

USER appuser

# JVM tuning for containers
ENV JAVA_OPTS="-XX:+UseContainerSupport \
                -XX:MaxRAMPercentage=75.0 \
                -XX:+UseG1GC \
                -XX:+UnlockExperimentalVMOptions \
                -XX:+UseCGroupMemoryLimitForHeap \
                -Djava.security.egd=file:/dev/./urandom"

EXPOSE 8080

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

ENTRYPOINT ["dumb-init", "--"]
CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

Build Script

#!/bin/bash
# build.sh

set -e

# Variables
IMAGE_NAME="myapp"
VERSION=${1:-latest}
REGISTRY="your-registry.com"

echo "Building $IMAGE_NAME:$VERSION"

# Build with BuildKit
DOCKER_BUILDKIT=1 docker build \
    --target runtime \
    --tag $IMAGE_NAME:$VERSION \
    --tag $IMAGE_NAME:latest \
    --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
    --build-arg VERSION=$VERSION \
    --build-arg VCS_REF=$(git rev-parse --short HEAD) \
    .

# Tag for registry
docker tag $IMAGE_NAME:$VERSION $REGISTRY/$IMAGE_NAME:$VERSION
docker tag $IMAGE_NAME:latest $REGISTRY/$IMAGE_NAME:latest

echo "Build completed: $IMAGE_NAME:$VERSION"

# Optional: Push to registry
if [ "$2" = "push" ]; then
    echo "Pushing to registry..."
    docker push $REGISTRY/$IMAGE_NAME:$VERSION
    docker push $REGISTRY/$IMAGE_NAME:latest
    echo "Push completed"
fi

Spring Boot Docker Integration

Maven Plugin Configuration

<!-- pom.xml -->
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <version>3.2.0</version>
    <configuration>
        <image>
            <name>myapp:${project.version}</name>
            <env>
                <BPE_DELIM_JAVA_TOOL_OPTIONS> </BPE_DELIM_JAVA_TOOL_OPTIONS>
                <BPE_APPEND_JAVA_TOOL_OPTIONS>-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0</BPE_APPEND_JAVA_TOOL_OPTIONS>
            </env>
        </image>
        <docker>
            <publishRegistry>
                <username>${docker.registry.username}</username>
                <password>${docker.registry.password}</password>
                <url>${docker.registry.url}</url>
            </publishRegistry>
        </docker>
    </configuration>
</plugin>
# Build with Spring Boot plugin
mvn spring-boot:build-image

# Build with custom name
mvn spring-boot:build-image -Dspring-boot.build-image.imageName=myapp:v1.0.0

Gradle Plugin Configuration

// build.gradle.kts
plugins {
    id("org.springframework.boot") version "3.2.0"
    id("io.spring.dependency-management") version "1.1.4"
}

tasks.named<org.springframework.boot.gradle.tasks.bundling.BootBuildImage>("bootBuildImage") {
    imageName = "myapp:${project.version}"
    environment = mapOf(
        "BPE_DELIM_JAVA_TOOL_OPTIONS" to " ",
        "BPE_APPEND_JAVA_TOOL_OPTIONS" to "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
    )

    docker {
        publishRegistry {
            username = project.findProperty("docker.registry.username") as String?
            password = project.findProperty("docker.registry.password") as String?
            url = project.findProperty("docker.registry.url") as String?
        }
    }
}

Docker Networking

Application Configuration

// Java application configuration for Docker
@Configuration
public class DockerConfig {

    @Value("${spring.datasource.url:jdbc:mysql://localhost:3306/myapp}")
    private String datasourceUrl;

    @Value("${spring.redis.host:localhost}")
    private String redisHost;

    @Bean
    @ConfigurationProperties("spring.datasource")
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(datasourceUrl);
        config.setMaximumPoolSize(20);
        config.setMinimumIdle(5);
        config.setConnectionTimeout(30000);
        config.setIdleTimeout(600000);
        config.setMaxLifetime(1800000);
        return new HikariDataSource(config);
    }

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName(redisHost);
        config.setPort(6379);

        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
                .commandTimeout(Duration.ofSeconds(2))
                .shutdownTimeout(Duration.ZERO)
                .build();

        return new LettuceConnectionFactory(config, clientConfig);
    }
}

Custom Network

# Create custom network
docker network create --driver bridge myapp-network

# Run containers on custom network
docker run -d --name myapp-db --network myapp-network mysql:8.0
docker run -d --name myapp-redis --network myapp-network redis:7-alpine
docker run -d --name myapp --network myapp-network -p 8080:8080 myapp:latest

Docker Monitoring và Logging

Application Metrics

// Micrometer metrics for Docker
@Component
public class DockerMetrics {

    private final MeterRegistry meterRegistry;
    private final Counter containerRestarts;
    private final Gauge memoryUsage;

    public DockerMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.containerRestarts = Counter.builder("container.restarts")
                .description("Number of container restarts")
                .register(meterRegistry);

        this.memoryUsage = Gauge.builder("jvm.memory.container.usage")
                .description("Container memory usage percentage")
                .register(meterRegistry, this, DockerMetrics::getMemoryUsagePercentage);
    }

    private double getMemoryUsagePercentage() {
        Runtime runtime = Runtime.getRuntime();
        long maxMemory = runtime.maxMemory();
        long totalMemory = runtime.totalMemory();
        long freeMemory = runtime.freeMemory();
        long usedMemory = totalMemory - freeMemory;

        return (double) usedMemory / maxMemory * 100;
    }

    @EventListener
    public void onApplicationReady(ApplicationReadyEvent event) {
        meterRegistry.gauge("container.startup.time", 
                System.currentTimeMillis() - event.getTimestamp());
    }
}

Logging Configuration

# application-docker.yml
logging:
  level:
    com.example: INFO
    org.springframework: WARN
    org.hibernate: WARN
  pattern:
    console: "%d{ISO8601} [%thread] %-5level [%logger{36}] - %msg%n"
  appender:
    console:
      target: SYSTEM_OUT

management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus,info
  endpoint:
    health:
      show-details: always
  metrics:
    export:
      prometheus:
        enabled: true

Docker Compose với Monitoring

# docker-compose.monitoring.yml
version: '3.8'

services:
  app:
    image: myapp:latest
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=docker
    networks:
      - monitoring
    labels:
      - "prometheus.io/scrape=true"
      - "prometheus.io/port=8080"
      - "prometheus.io/path=/actuator/prometheus"

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
    networks:
      - monitoring

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana_data:/var/lib/grafana
      - ./docker/grafana/dashboards:/var/lib/grafana/dashboards
    networks:
      - monitoring

  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686"
      - "14268:14268"
    environment:
      - COLLECTOR_OTLP_ENABLED=true
    networks:
      - monitoring

volumes:
  grafana_data:

networks:
  monitoring:
    driver: bridge

Security Best Practices

Secure Dockerfile

FROM eclipse-temurin:17-jre-alpine

# Update packages và remove package manager
RUN apk update && \
    apk upgrade && \
    apk add --no-cache dumb-init curl && \
    apk del apk-tools && \
    rm -rf /var/cache/apk/* /tmp/* /var/tmp/*

# Create non-root user với specific UID/GID
RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup -h /app

# Set proper file permissions
COPY --chown=appuser:appgroup app.jar /app/app.jar

# Switch to non-root user
USER appuser

WORKDIR /app

# Remove unnecessary capabilities
RUN setcap -r /app/app.jar || true

# Security headers
ENV JAVA_OPTS="-Djava.security.egd=file:/dev/./urandom \
                -Dnetworkaddress.cache.ttl=60 \
                -XX:+UseContainerSupport"

EXPOSE 8080

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

ENTRYPOINT ["dumb-init", "--"]
CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

Docker Security Configuration

# Run container với security constraints
docker run -d \
    --name myapp \
    --user 1001:1001 \
    --read-only \
    --tmpfs /tmp \
    --tmpfs /var/tmp \
    --no-new-privileges \
    --cap-drop ALL \
    --cap-add NET_BIND_SERVICE \
    --security-opt no-new-privileges:true \
    --security-opt seccomp=default \
    -p 8080:8080 \
    myapp:latest

Best Practices

Image Optimization

  1. Multi-stage builds: Giảm image size
  2. Alpine base images: Sử dụng lightweight Linux distribution
  3. Layer caching: Optimize Dockerfile để maximize cache hits
  4. Non-root user: Chạy application với non-root user

Security

  1. Minimal base images: Sử dụng distroless hoặc alpine images
  2. Security scanning: Scan images cho vulnerabilities
  3. Secrets management: Không hardcode secrets trong images
  4. Runtime security: Sử dụng security constraints

Performance

  1. JVM tuning: Optimize JVM cho container environment
  2. Resource limits: Set appropriate CPU và memory limits
  3. Health checks: Implement proper health checks
  4. Monitoring: Monitor container metrics và logs

Docker containerization giúp Java applications có tính consistency, scalability, và deployment flexibility cao trong modern infrastructure.