Docker 容器化部署实战
Docker 容器化已成为 Java 应用部署的标准方式。通过容器化,可以实现环境一致性、快速部署、弹性伸缩。本文从实战角度,详解 Java 应用 Docker 容器化的全流程,包括 Dockerfile 编写、镜像优化、多阶段构建、容器编排等最佳实践。
一、为什么选择 Docker
1.1 传统部署痛点
---
config:
theme: forest
---
mindmap
root((传统部署问题))
环境不一致
开发环境:Java 11 + MySQL 8
测试环境:Java 8 + MySQL 5.7
生产环境:Java 11 + MySQL 8
结果:测试通过,上线失败
依赖冲突
应用 A:需要 Guava 20
应用 B:需要 Guava 30
同一服务器无法共存
部署复杂
手动安装 JDK
配置环境变量
下载应用包
启动脚本
耗时:30 分钟 +
资源浪费
每台服务器独立环境
无法充分利用资源
扩缩容慢
1.2 Docker 优势
| 对比项 | 虚拟机 | Docker | 传统部署 |
|---|---|---|---|
| 启动时间 | 分钟级 | 秒级 | 分钟级 |
| 镜像大小 | GB 级 | MB 级 | - |
| 性能损耗 | 5-15% | <5% | 0% |
| 隔离性 | 完全隔离 | 进程隔离 | 无隔离 |
| 资源利用率 | 低 | 高 | 中 |
二、Dockerfile 编写
2.0 Docker 架构
---
config:
theme: forest
---
graph TB
subgraph 开发环境
A[开发者] -->|构建 | B[Dockerfile]
B -->|生成 | C[Docker 镜像]
end
subgraph 镜像仓库
C -->|推送 | D[(Docker Registry)]
end
subgraph 运行环境
D -->|拉取 | E[Docker 容器]
E -->|运行 | F[Java 应用]
end
subgraph 编排管理
E -->|管理 | G[Docker Compose]
E -->|编排 | H[Kubernetes]
end
2.1 基础 Dockerfile
# 基础镜像(使用 OpenJDK 11)
FROM openjdk:11-jre-slim
# 维护者信息
LABEL maintainer="your.name@example.com"
# 设置工作目录
WORKDIR /app
# 复制应用 jar 包
COPY target/myapp.jar app.jar
# 暴露端口
EXPOSE 8080
# JVM 参数优化
ENV JAVA_OPTS="-Xms512m -Xmx512m -XX:+UseG1GC"
# 启动命令
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
构建和运行:
# 构建镜像
docker build -t myapp:1.0 .
# 运行容器
docker run -d \
--name myapp \
-p 8080:8080 \
-e JAVA_OPTS="-Xms1g -Xmx1g" \
myapp:1.0
# 查看日志
docker logs -f myapp
2.2 多阶段构建
问题:基础镜像包含完整 JDK,镜像体积大(约 500MB)
解决:多阶段构建,分离构建环境和运行环境
# ========== 第一阶段:构建 ==========
FROM maven:3.8-openjdk-11 AS builder
WORKDIR /build
# 复制 pom.xml(利用 Docker 缓存层)
COPY pom.xml .
# 下载依赖(缓存依赖层)
RUN mvn dependency:go-offline -B
# 复制源代码
COPY src ./src
# 编译打包
RUN mvn clean package -DskipTests
# ========== 第二阶段:运行 ==========
FROM openjdk:11-jre-slim
WORKDIR /app
# 创建非 root 用户(安全最佳实践)
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
# 从构建阶段复制 jar 包
COPY --from=builder /build/target/myapp.jar app.jar
# 设置文件权限
RUN chown -R appuser:appgroup /app
# 切换到非 root 用户
USER appuser
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# 启动命令
ENTRYPOINT ["java", "-Xms512m", "-Xmx512m", "-jar", "app.jar"]
镜像大小对比:
单阶段构建:openjdk:11-jre-slim → 约 450MB
多阶段构建:builder + runtime → 约 200MB(减少 55%)
2.3 Alpine 镜像优化
进一步减小镜像:使用 Alpine 基础镜像
# 构建阶段
FROM maven:3.8-eclipse-temurin-11 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
# 运行阶段(使用 Alpine)
FROM eclipse-temurin:11-jre-alpine
WORKDIR /app
# 安装 curl(用于健康检查)
RUN apk add --no-cache curl
# 创建用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /build/target/myapp.jar app.jar
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-Xms512m", "-Xmx512m", "-XX:+UseG1GC", "-jar", "app.jar"]
镜像大小:
Alpine 镜像:约 120MB(比 slim 再减少 70%)
注意:Alpine 使用 musl libc,某些 native 库可能不兼容,需测试验证。
三、Docker Compose 编排
3.1 单机编排
场景:开发环境,快速启动应用 + 数据库 + 缓存
# docker-compose.yml
version: '3.8'
services:
# MySQL 数据库
mysql:
image: mysql:8.0
container_name: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: myapp
MYSQL_USER: appuser
MYSQL_PASSWORD: app123
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
networks:
- app-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
# Redis 缓存
redis:
image: redis:7-alpine
container_name: redis
restart: always
command: redis-server --appendonly yes
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- app-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# Java 应用
app:
build:
context: .
dockerfile: Dockerfile
container_name: myapp
restart: always
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
environment:
SPRING_PROFILES_ACTIVE: docker
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/myapp?useSSL=false&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_USERNAME: appuser
SPRING_DATASOURCE_PASSWORD: app123
SPRING_REDIS_HOST: redis
JAVA_OPTS: >-
-Xms512m -Xmx512m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/heapdump.hprof
ports:
- "8080:8080"
volumes:
- ./logs:/logs
networks:
- app-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 40s
# 数据卷
volumes:
mysql_data:
driver: local
redis_data:
driver: local
# 网络
networks:
app-network:
driver: bridge
使用命令:
# 启动所有服务
docker-compose up -d
# 查看状态
docker-compose ps
# 查看日志
docker-compose logs -f app
# 停止所有服务
docker-compose down
# 停止并删除数据卷(谨慎使用)
docker-compose down -v
# 重新构建并启动
docker-compose up -d --build
3.2 多环境配置
开发环境:
# docker-compose.dev.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
environment:
SPRING_PROFILES_ACTIVE: dev
DEBUG_ENABLED: "true"
volumes:
# 挂载源代码,实现热更新
- ./src:/app/src
- ./logs:/logs
ports:
- "8080:8080"
- "5005:5005" # 远程调试端口
生产环境:
# docker-compose.prod.yml
version: '3.8'
services:
app:
image: myapp:1.0 # 使用预构建镜像
deploy:
replicas: 3 # 3 个副本
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '1'
memory: 1G
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
environment:
SPRING_PROFILES_ACTIVE: prod
JAVA_OPTS: >-
-Xms1g -Xmx1g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "3"
多环境启动:
# 开发环境
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# 生产环境
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
四、Kubernetes 部署
4.1 基础部署
Deployment 配置:
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
version: v1
spec:
containers:
- name: myapp
image: myapp:1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: http
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: JAVA_OPTS
value: "-Xms512m -Xmx512m -XX:+UseG1GC"
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
volumeMounts:
- name: logs
mountPath: /logs
volumes:
- name: logs
emptyDir: {}
4.2 Service 配置
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
selector:
app: myapp
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP # 内部访问
# type: LoadBalancer # 外部访问(云厂商)
# type: NodePort # 节点端口访问
4.3 ConfigMap 和 Secret
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
data:
application.yml: |
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://mysql:3306/myapp
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: redis
port: 6379
logging:
level:
com.example: INFO
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: myapp-secret
type: Opaque
stringData:
DB_USERNAME: appuser
DB_PASSWORD: app123
JWT_SECRET: your-secret-key-here
使用配置:
# 在 Deployment 中引用
spec:
containers:
- name: myapp
envFrom:
- configMapRef:
name: myapp-config
- secretRef:
name: myapp-secret
4.4 部署命令
# 应用配置
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/secret.yaml
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
# 查看状态
kubectl get deployments
kubectl get pods
kubectl get services
# 查看日志
kubectl logs -f deployment/myapp
# 扩容
kubectl scale deployment myapp --replicas=5
# 滚动更新
kubectl set image deployment/myapp myapp=myapp:1.1
kubectl rollout status deployment/myapp
# 回滚
kubectl rollout undo deployment/myapp
# 删除
kubectl delete -f k8s/
五、CI/CD 集成
5.1 GitHub Actions
# .github/workflows/docker-build.yml
name: Docker Build and Push
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn clean package -DskipTests
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
myapp:${{ github.sha }}
myapp:latest
cache-from: type=registry,ref=myapp:buildcache
cache-to: type=registry,ref=myapp:buildcache,mode=max
- name: Deploy to Kubernetes
if: github.ref == 'refs/heads/main'
run: |
kubectl set image deployment/myapp myapp=myapp:${{ github.sha }}
5.2 Jenkins Pipeline
// Jenkinsfile
pipeline {
agent any
environment {
DOCKER_IMAGE = "myapp"
DOCKER_REGISTRY = "registry.example.com"
}
stages {
stage('Checkout') {
steps {
git branch: 'main', url: 'https://github.com/your/repo.git'
}
}
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('Docker Build') {
steps {
script {
docker.build("${DOCKER_IMAGE}:${BUILD_ID}")
}
}
}
stage('Test') {
steps {
sh 'mvn test'
}
}
stage('Push') {
steps {
script {
docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-credentials') {
docker.image("${DOCKER_IMAGE}:${BUILD_ID}").push()
docker.image("${DOCKER_IMAGE}:${BUILD_ID}").push('latest')
}
}
}
}
stage('Deploy') {
steps {
sh '''
kubectl set image deployment/myapp myapp=${DOCKER_REGISTRY}/${DOCKER_IMAGE}:${BUILD_ID}
kubectl rollout status deployment/myapp
'''
}
}
}
post {
always {
cleanWs()
}
failure {
echo 'Build failed!'
}
success {
echo 'Build success!'
}
}
}
六、最佳实践
6.1 Dockerfile 优化
# ✅ 推荐做法
# 1. 使用具体版本标签
FROM openjdk:11.0.18-jre-slim # ✅
FROM openjdk:latest # ❌
# 2. 多阶段构建
FROM maven AS builder
FROM openjdk:11-jre-slim
# 3. 合并 RUN 指令(减少镜像层)
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
# 4. 使用 .dockerignore
# .dockerignore
target/
.git/
*.md
.gitignore
# 5. 非 root 用户
RUN useradd -r appuser
USER appuser
# 6. 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8080/health || exit 1
6.2 JVM 参数优化
# 容器化 JVM 参数
java -XX:+UseContainerSupport \ # 启用容器支持(Java 8u191+ 默认开启)
-XX:MaxRAMPercentage=75.0 \ # 最大使用容器内存的 75%
-XX:+UseG1GC \ # 使用 G1 垃圾收集器
-XX:MaxGCPauseMillis=200 \ # 最大 GC 暂停时间
-XX:+HeapDumpOnOutOfMemoryError \ # OOM 时 dump 堆
-XX:HeapDumpPath=/logs/ \ # dump 文件路径
-jar app.jar
# 容器资源限制
docker run -m 1g --cpus=1.0 myapp
6.3 日志处理
# Docker 日志配置
services:
app:
logging:
driver: "json-file"
options:
max-size: "100m" # 单个日志文件最大 100MB
max-file: "3" # 保留 3 个日志文件
// Spring Boot 日志配置(application.yml)
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: /logs/app.log
max-size: 100MB
max-history: 30
6.4 安全实践
# 1. 使用官方镜像
FROM openjdk:11-jre-slim # ✅
FROM some-random-image # ❌
# 2. 扫描镜像漏洞
docker scan myapp:1.0
# 3. 不存储敏感信息
# Dockerfile 中不要写密码
ENV DB_PASSWORD=secret123 # ❌
# 使用 Secret 管理
# kubectl create secret generic db-secret --from-literal=password=secret123
# 4. 最小权限原则
USER appuser # 非 root 用户
# 5. 只读文件系统(可选)
securityContext:
readOnlyRootFilesystem: true
七、常见问题
7.1 镜像构建失败
问题 1:Maven 依赖下载慢
# 使用国内镜像
FROM maven:3.8-openjdk-11
RUN mkdir -p /root/.m2 && \
echo '<settings><mirrors><mirror><id>aliyun</id><url>https://maven.aliyun.com/repository/public</url><mirrorOf>central</mirrorOf></mirror></mirrors></settings>' > /root/.m2/settings.xml
问题 2:镜像体积过大
# 查看镜像层
docker history myapp:1.0
# 使用多阶段构建
# 使用 Alpine 基础镜像
# 清理缓存:rm -rf /var/lib/apt/lists/*
7.2 容器启动失败
问题 1:OOMKilled
# 增加内存限制
resources:
limits:
memory: "2Gi"
requests:
memory: "1Gi"
# 调整 JVM 参数
JAVA_OPTS: "-Xms1g -Xmx1g" # 不超过容器内存的 75%
问题 2:健康检查失败
# 增加启动等待时间
livenessProbe:
initialDelaySeconds: 120 # 从 60s 增加到 120s
periodSeconds: 10
7.3 网络连接问题
问题:容器无法访问外部服务
# 检查 DNS
docker run --dns 8.8.8.8 myapp
# 检查网络
docker network ls
docker network inspect bridge
总结
Java 应用 Docker 容器化要点:
- Dockerfile 优化:多阶段构建、Alpine 镜像、非 root 用户
- 编排工具:Docker Compose(单机)、Kubernetes(集群)
- CI/CD 集成:GitHub Actions、Jenkins 自动化部署
- 最佳实践:镜像优化、JVM 调优、日志处理、安全加固
- 问题排查:镜像构建、容器启动、网络连接
容器化不是一蹴而就的,持续优化才能发挥最大价值。