Docker 容器化 Spring Boot 应用:镜像优化与多阶段构建

将 Spring Boot 应用容器化是现代部署的基本功。但默认的 Dockerfile 打包出来的镜像体积大、启动慢、安全性差。本文介绍如何构建生产级的 Spring Boot Docker 镜像。

基础 Dockerfile 编写

项目结构

1
2
3
4
spring-boot-app/
├── src/
├── pom.xml
└── Dockerfile

基础版本

1
2
3
4
5
6
7
8
9
10
# Dockerfile
FROM openjdk:17

WORKDIR /app

COPY target/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

问题:

  • 镜像体积大(openjdk:17 约 500MB)
  • 没有指定 JRE/JDK 区别
  • 每次代码变更都需要 COPY 全部依赖

多阶段构建原理

多阶段构建使用多个 FROM 语句,前一阶段的产物可以复制到后一阶段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 第一阶段:构建
FROM maven:3.9-eclipse-temurin-17 AS builder

WORKDIR /build

COPY pom.xml .
RUN mvn dependency:go-offline # 下载依赖

COPY src ./src
RUN mvn package -DskipTests

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

WORKDIR /app

COPY --from=builder /build/target/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

好处

  • 最终镜像只包含运行时需要的内容
  • 不包含 Maven、源代码等
  • 镜像体积大幅减小

镜像大小优化技巧

1. 使用 Alpine 基础镜像

1
2
3
4
5
# 体积对比
# openjdk:17 ~500MB
# eclipse-temurin:17 ~200MB
# eclipse-temurin:17-jre-alpine ~180MB
# alpine + jre ~100MB
1
2
3
4
5
6
7
FROM eclipse-temurin:17-jre-alpine

# Alpine 需要加时区数据
RUN apk add --no-cache tzdata

# 设置时区
ENV TZ=Asia/Shanghai

2. 使用 distroless 镜像(Google)

1
2
3
4
5
6
# 更小、更安全的运行时镜像
FROM gcr.io/distroless/java17-debian11

COPY target/*.jar app.jar

ENTRYPOINT ["app.jar"]

distroless 镜像特点:

  • 只包含运行时需要的内容
  • 没有 shell、没有包管理器
  • 更安全(攻击面小)

3. 只复制必要文件

1
2
3
4
5
# 错误:复制整个项目
COPY . .

# 正确:只复制 jar
COPY target/*.jar app.jar

4. 利用层缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 利用 Docker 层缓存,先复制依赖文件(不变的部分)
FROM maven:3.9-eclipse-temurin-17 AS builder

WORKDIR /build

# 先复制 pom.xml
COPY pom.xml .
# 下载依赖(这一层可以缓存)
RUN mvn dependency:go-offline

# 再复制源代码
COPY src ./src
# 构建(这一层会在代码变更时才重新构建)
RUN mvn package -DskipTests

完整优化版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# ============================================
# Stage 1: Build
# ============================================
FROM maven:3.9-eclipse-temurin-17 AS builder

WORKDIR /build

# 利用层缓存:先复制依赖
COPY pom.xml .
RUN mvn dependency:go-offline -B

# 再复制源码
COPY src ./src

# 构建
RUN mvn package -DskipTests -B

# ============================================
# Stage 2: Runtime
# ============================================
FROM eclipse-temurin:17-jre-alpine

# Alpine 需要时区数据
RUN apk add --no-cache tzdata

# 创建一个非 root 用户
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup

WORKDIR /app

# 从构建阶段复制 jar
COPY --from=builder /build/target/*.jar app.jar

# 修改文件所有者
RUN chown -R appuser:appgroup /app

# 切换到非 root 用户
USER appuser

# 暴露端口
EXPOSE 8080

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

ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

.dockerignore 配置

.dockerignore 防止不需要的文件进入镜像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# .dockerignore

# Git
.git
.gitignore

# IDE
.idea/
*.iml
.vscode/

# 构建产物(会重新构建,不需要上传)
target/
build/
*.class
*.jar

# 测试文件
src/test/
*.test
*.spec

# 文档
*.md
docs/

# Docker 相关
Dockerfile
docker-compose*.yml
.docker/

# 日志
*.log
logs/

# 环境配置(不应该被打包)
.env
.env.*
*.env

# 其他
.DS_Store
Thumbs.db

注意.dockerignore 放在项目根目录,不是 Dockerfile 同级。

Jib 无需 Dockerfile

Google Jib 是更现代的构建方案,不需要写 Dockerfile:

Maven 插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<from>
<image>eclipse-temurin:17-jre-alpine</image>
</from>
<to>
<image>registry.example.com/myapp:${project.version}</image>
<tags>
<tag>latest</tag>
<tag>${maven.build.timestamp}</tag>
</tags>
</to>
<container>
<jvmFlags>
<jvmFlag>-XX:+UseContainerSupport</jvmFlag>
<jvmFlag>-XX:MaxRAMPercentage=75.0</jvmFlag>
</jvmFlags>
<ports>
<port>8080</port>
</port>
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
</container>
</configuration>
</plugin>

使用

1
2
3
4
5
6
7
8
# 构建镜像(推送到远程)
mvn compile jib:build

# 构建到本地 Docker
mvn compile jib:dockerBuild

# 查看日志
mvn jib:log -Djib.container.command=sleep,600

Jib 的优势:

  • :利用层缓存,只上传变化的层
  • 简单:不需要 Dockerfile
  • 安全:不需要 Docker daemon 权限

实战:生产级镜像构建

完整项目结构

1
2
3
4
5
6
7
spring-boot-app/
├── src/
├── pom.xml
├── Dockerfile
├── .dockerignore
└── docker/
└── entrypoint.sh

entrypoint.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh

# JVM 参数优化容器内运行
JAVA_OPTS="-XX:+UseContainerSupport"
JAVA_OPTS="$JAVA_OPTS -XX:MaxRAMPercentage=75.0"
JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC"
JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError"

# GC 日志
JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:file=/app/logs/gc.log"

# 应用配置
SPRING_OPTS="$SPRING_OPTS --server.tomcat.threads.max=200"
SPRING_OPTS="$SPRING_OPTS --server.tomcat.threads.min-spare=10"

echo "Starting application..."
echo "Java options: $JAVA_OPTS"
echo "Spring options: $SPRING_OPTS"

exec java $JAVA_OPTS $SPRING_OPTS -jar /app/app.jar "$@"

Dockerfile(生产级)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# ============================================
# Stage 1: Build
# ============================================
FROM maven:3.9-eclipse-temurin-17 AS builder

WORKDIR /build

# 复制依赖(利用层缓存)
COPY pom.xml .
RUN mvn dependency:go-offline -B

# 复制源码
COPY src ./src

# 构建
RUN mvn package -DskipTests -B && \
# 剥离不需要的文件,减小 jar
cd target/dependency && \
mv ../*.jar app.jar

# ============================================
# Stage 2: Runtime
# ============================================
FROM eclipse-temurin:17-jre-alpine

# 安全更新
RUN apk upgrade --no-cache && \
apk add --no-cache wget ca-certificates

# 创建目录和用户
RUN mkdir -p /app/logs /app/config && \
addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup

WORKDIR /app

# 复制 jar
COPY --from=builder /build/target/dependency/*.jar app.jar

# 复制配置文件(如果需要)
# COPY docker/app-config.yml /app/config/app.yml

# 设置权限
RUN chown -R appuser:appgroup /app

USER appuser

EXPOSE 8080

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

ENTRYPOINT ["sh", "/app/entrypoint.sh"]

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
version: '3.8'

services:
app:
build:
context: .
dockerfile: Dockerfile
image: myapp:latest
container_name: myapp
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- JAVA_OPTS=-Xmx512m
volumes:
- ./logs:/app/logs
- ./config:/app/config:ro
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 512M

总结

Spring Boot Docker 镜像优化要点:

优化项 方案 效果
基础镜像 eclipse-temurin:17-jre-alpine ~180MB
多阶段构建 builder + runtime 去掉 Maven、源码
层缓存 pom.xml 优先 COPY 构建加速
非 root 用户 adduser 安全性
健康检查 actuator + HEALTHCHECK 可观测性
JVM 容器支持 -XX:+UseContainerSupport 合理使用内存

推荐构建方案:

  • 简单项目:使用 Jib Maven 插件
  • 复杂项目:手写优化 Dockerfile
  • 生产环境:多阶段构建 + 非 root + 健康检查