Skip to content
第 78 / 250 章后端⏱ 10 分钟阅读

第 78 章:Docker 镜像构建

学习目标

  • 掌握 Spring Boot 应用的 Dockerfile 编写
  • 学会多阶段构建、镜像瘦身、加速构建
  • 实现 Docker Compose 一键启动全套依赖

一、为什么需要 Docker?

Docker 解决:把代码 + 运行时 + 依赖 + 配置打包成一个镜像,任何环境运行都一样

二、Dockerfile 基础

最简版本

dockerfile
# 基础镜像
FROM eclipse-temurin:17-jre-alpine

# 工作目录
WORKDIR /app

# 拷贝 jar
COPY target/app.jar /app/app.jar

# 启动命令
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

多阶段构建(推荐)

dockerfile
# ============ 第一阶段:构建 ============
FROM eclipse-temurin:17-jdk-alpine AS builder

WORKDIR /build

# ① 先拷贝 pom.xml 单独下载依赖(利用 Docker 缓存)
COPY pom.xml .
RUN ./mvnw dependency:go-offline -B

# ② 拷贝源码编译
COPY src ./src
RUN ./mvnw package -DskipTests -B

# ============ 第二阶段:运行 ============
FROM eclipse-temurin:17-jre-alpine

WORKDIR /app

# ③ 时区设置(避免容器内时区错乱)
RUN apk add --no-cache tzdata && \
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone && \
    apk del tzdata

# ④ 非 root 用户(安全)
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring

# ⑤ 从构建阶段拷贝 jar
COPY --from=builder /build/target/app.jar /app/app.jar

# ⑥ JVM 参数
ENV JAVA_OPTS="-Xms512m -Xmx512m -XX:+UseG1GC -XX:MaxRAMPercentage=75.0"

# ⑦ 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
    CMD wget -q --spider http://localhost:8080/actuator/health || exit 1

EXPOSE 8080

# ⑧ 启动
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]

三、镜像瘦身

镜像大小对比

openjdk:17-jdk            850MB  ❌
eclipse-temurin:17-jdk    600MB
eclipse-temurin:17-jre    280MB
eclipse-temurin:17-jre-alpine  180MB  ✅ 推荐
bellsoft/liberica-runtime:17-jre-alpine  200MB

.dockerignore

gitignore
# .dockerignore
target/
.git/
.idea/
.vscode/
*.iml
*.log
.mvn/
mvnw.cmd
README.md
HELP.md

分层压缩

dockerfile
# 启动时解压(Spring Boot 自带)
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

# 用 Spring Boot 的 Layered 启动特性(进一步优化)
# pom.xml 加:
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <layers>
                    <enabled>true</enabled>
                </layers>
            </configuration>
        </plugin>
    </plugins>
</build>
dockerfile
# Dockerfile 配合分层
FROM eclipse-temurin:17-jre-alpine AS builder
WORKDIR /build
COPY target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /build/dependencies/ ./
COPY --from=builder /build/spring-boot-loader/ ./
COPY --from=builder /build/snapshot-dependencies/ ./
COPY --from=builder /build/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

效果:依赖层变更不频繁,构建和拉取都加速。

四、构建镜像

bash
# 构建
docker build -t taskflow:1.0.0 .

# 指定 Dockerfile
docker build -f Dockerfile -t taskflow:1.0.0 .

# 多平台构建(amd64 + arm64)
docker buildx build --platform linux/amd64,linux/arm64 -t taskflow:1.0.0 --push .

docker buildx 高级

bash
# 创建 builder
docker buildx create --name mybuilder --use
docker buildx inspect --bootstrap

# 推到多个仓库
docker buildx build \
    --platform linux/amd64,linux/arm64 \
    -t registry.cn-hangzhou.aliyuncs.com/taskflow/app:1.0.0 \
    -t taskflow:1.0.0 \
    --push .

五、Docker Compose

单服务

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

services:
  app:
    image: taskflow:1.0.0
    container_name: taskflow-app
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - DB_HOST=mysql
      - DB_PASSWORD=secret
      - REDIS_HOST=redis
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - taskflow-net

全栈服务(MySQL + Redis + RabbitMQ + App)

yaml
version: '3.8'

services:
  mysql:
    image: mysql:8.0
    container_name: taskflow-mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: taskflow
      TZ: Asia/Shanghai
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --default-authentication-plugin=mysql_native_password
    volumes:
      - mysql-data:/var/lib/mysql
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot123"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - taskflow-net

  redis:
    image: redis:7-alpine
    container_name: taskflow-redis
    restart: unless-stopped
    command: redis-server --appendonly yes --requirepass redis123
    volumes:
      - redis-data:/data
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "redis123", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
    networks:
      - taskflow-net

  rabbitmq:
    image: rabbitmq:3-management-alpine
    container_name: taskflow-rabbitmq
    restart: unless-stopped
    environment:
      RABBITMQ_DEFAULT_USER: admin
      RABBITMQ_DEFAULT_PASS: rabbit123
    volumes:
      - rabbitmq-data:/var/lib/rabbitmq
    ports:
      - "5672:5672"
      - "15672:15672"    # 管理界面
    networks:
      - taskflow-net

  app:
    build:
      context: .
      dockerfile: Dockerfile
    image: taskflow:1.0.0
    container_name: taskflow-app
    restart: unless-stopped
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DB_HOST: mysql
      DB_PORT: 3306
      DB_NAME: taskflow
      DB_USER: root
      DB_PASSWORD: root123
      REDIS_HOST: redis
      REDIS_PORT: 6379
      REDIS_PASSWORD: redis123
      RABBITMQ_HOST: rabbitmq
      RABBITMQ_USER: admin
      RABBITMQ_PASSWORD: rabbit123
    ports:
      - "8080:8080"
    networks:
      - taskflow-net

volumes:
  mysql-data:
  redis-data:
  rabbitmq-data:

networks:
  taskflow-net:
    driver: bridge

常用命令

bash
# 启动
docker compose up -d

# 查看日志
docker compose logs -f app

# 查看状态
docker compose ps

# 停止
docker compose down

# 停止并删除数据卷
docker compose down -v

# 重新构建
docker compose up -d --build

# 扩缩容
docker compose up -d --scale app=3

# 进入容器
docker compose exec app sh
docker compose exec mysql mysql -uroot -proot123

六、JVM 在容器中的坑

坑 1:JVM 默认堆大小

bash
# 默认:JVM 看到 8GB 内存(宿主机内存),堆上限 25% = 2GB
# 实际:容器只有 1GB 内存 → OOMKilled
dockerfile
# ✅ 方案 1:用 MaxRAMPercentage(推荐)
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0"
# JVM 自动按容器内存的 75% 分配堆

# ✅ 方案 2:显式指定(不灵活)
ENV JAVA_OPTS="-Xms512m -Xmx512m"
yaml
# docker-compose.yml
services:
  app:
    deploy:
      resources:
        limits:
          memory: 1G             # K8s/docker 限制内存
    environment:
      JAVA_OPTS: "-XX:MaxRAMPercentage=75.0"

JVM 11+ 默认能感知 cgroup 内存。JDK 17 更完善。

坑 2:时区

dockerfile
# Alpine
RUN apk add --no-cache tzdata && \
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone

# 启动参数
ENV TZ=Asia/Shanghai

坑 3:hostname 变化导致 Token 失效

java
// JWT 用 hostname 签发,容器重启后 hostname 变了 → 之前的 token 全失效
// 解决:用机器唯一 ID(如 pod name)作为 issuer

七、镜像安全

bash
# ① 扫描漏洞
docker scout cves taskflow:1.0.0
trivy image taskflow:1.0.0

# ② 用 distroless 镜像(最小攻击面)
FROM gcr.io/distroless/java17-debian12
COPY target/app.jar /app/app.jar
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
# 镜像只包含 JRE 和你的应用,没有 shell、curl 等

# ③ 签名
cosign sign --key cosign.key taskflow:1.0.0

八、私有镜像仓库

bash
# 阿里云容器镜像服务
docker login registry.cn-hangzhou.aliyuncs.com
docker tag taskflow:1.0.0 registry.cn-hangzhou.aliyuncs.com/taskflow/app:1.0.0
docker push registry.cn-hangzhou.aliyuncs.com/taskflow/app:1.0.0

# Harbor 自建仓库
docker login harbor.taskflow.com
docker push harbor.taskflow.com/taskflow/app:1.0.0

九、镜像优化清单

优化项效果
jre 而非 jdk镜像小 70%
alpine镜像小 30%
多阶段构建镜像小 50%
.dockerignore减少上下文,加快构建
Layered 启动利用缓存,重写镜像加速
时区内置避免运行时调整
非 root 用户安全
健康检查K8s/Docker 自动重启

十、本章小结

要点关键
镜像eclipse-temurin:17-jre-alpine(小 + 安全)
Dockerfile多阶段构建、.dockerignoreUSER 非 root
JVM-XX:MaxRAMPercentage=75.0 适配容器
Composedepends_on + healthcheck
私有仓库阿里云 / Harbor
安全非 root + 漏洞扫描 + 镜像签名

动手练习

练习 1:基础题

为你的 Spring Boot 项目编写 Dockerfile,要求:

  • 多阶段构建
  • 使用 alpine 基础镜像
  • 时区设置为 Asia/Shanghai
  • 非 root 用户
  • 健康检查端点

构建镜像并验证容器内应用可访问。

练习 2:进阶题

编写 docker-compose.yml,启动:MySQL + Redis + RabbitMQ + 你的 Spring Boot 应用,一键拉起完整环境。

练习 3:思考题

你的镜像有 800MB,部署到 K8s 时拉镜像要 5 分钟。如何优化?分层缓存怎么用?


下一章第 79 章:CI/CD 流水线

本站基于 VitePress 构建 · 由 Codebook 团队维护