第 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 内存 → OOMKilleddockerfile
# ✅ 方案 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 | 多阶段构建、.dockerignore、USER 非 root |
| JVM | -XX:MaxRAMPercentage=75.0 适配容器 |
| Compose | depends_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 流水线 →