Skip to content
清晨的一缕阳光
返回

Docker 容器化部署实战

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 容器化要点:

  1. Dockerfile 优化:多阶段构建、Alpine 镜像、非 root 用户
  2. 编排工具:Docker Compose(单机)、Kubernetes(集群)
  3. CI/CD 集成:GitHub Actions、Jenkins 自动化部署
  4. 最佳实践:镜像优化、JVM 调优、日志处理、安全加固
  5. 问题排查:镜像构建、容器启动、网络连接

容器化不是一蹴而就的,持续优化才能发挥最大价值。


分享这篇文章到:

上一篇文章
CI/CD 流水线设计实战
下一篇文章
中国人口老龄化与养老服务发展报告(2026最新权威数据版)