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
- Multi-stage builds: Giảm image size
- Alpine base images: Sử dụng lightweight Linux distribution
- Layer caching: Optimize Dockerfile để maximize cache hits
- Non-root user: Chạy application với non-root user
Security
- Minimal base images: Sử dụng distroless hoặc alpine images
- Security scanning: Scan images cho vulnerabilities
- Secrets management: Không hardcode secrets trong images
- Runtime security: Sử dụng security constraints
Performance
- JVM tuning: Optimize JVM cho container environment
- Resource limits: Set appropriate CPU và memory limits
- Health checks: Implement proper health checks
- Monitoring: Monitor container metrics và logs
Docker containerization giúp Java applications có tính consistency, scalability, và deployment flexibility cao trong modern infrastructure.