跳到主内容
创建时间: 2026-09-16最后更新: 2026-09-16

目前应用可以通过 Maven 打包成可执行 JAR,也能从环境变量读取数据库和监控配置。生产部署还需要一份可重复的运行环境:明确 Java 版本、文件内容、启动命令和进程用户,并让同一镜像在不同环境接收不同配置。

本章把心跳应用制作成 Docker 镜像。我们会先区分 JAR 与镜像,再编写多阶段、分层 Dockerfile,最后让应用和 PostgreSQL 通过 Compose 网络协作。镜像只保存程序与运行时,密码仍在启动时注入。

1. JAR 与镜像

Maven 打包产生的 JAR 包含应用类、资源和依赖,运行它还需要操作系统中存在兼容的 Java:

terminal.bash
1
./mvnw clean package
2
java -jar target/spring-0.0.1-SNAPSHOT.jar

容器镜像在此基础上再固定运行时文件系统和启动命令:

deployment-artifacts.txt
1
源代码 + pom.xml
2
-> Maven 构建
3
-> 可执行 JAR
4
-> Docker 构建
5
-> 应用镜像
6
-> 容器实例

镜像是只读模板,容器是镜像的一次运行实例。同一个镜像可以启动多个容器,每个实例通过环境变量、网络和资源限制获得不同运行配置。

不要在容器启动时重新编译源码。构建阶段应该产出已经通过测试的镜像,部署阶段只拉取这个不可变产物并提供环境配置,避免同一版本在不同机器重新解析出不同依赖。

2. 构建上下文

Docker 构建会把项目目录作为构建上下文发送给引擎。先创建 .dockerignore,排除无关或敏感内容:

.dockerignore
1
target
2
.git
3
.idea
4
.vscode
5
*.log
6
.env
7
compose*.yaml

target 会由镜像构建阶段重新产生,不需要从宿主机复制;.git 和编辑器目录只会增大上下文;.env 可能包含凭据,不应该进入镜像层。

.dockerignore 不是秘密管理工具。已经被 COPY 写入某个镜像层的文件,即使在后续层删除,也可能仍能从历史层恢复。敏感值从一开始就不应进入构建上下文和 Dockerfile。

为了让依赖层能够被缓存,Dockerfile 应先复制变化较少的 Maven 配置,再复制经常变化的源码。修改一个 Controller 时,Docker 就不必在每次构建中重新下载全部依赖。

3. 分层镜像

创建多阶段 Dockerfile:

Dockerfile
01
FROM eclipse-temurin:21-jdk-jammy AS builder
02
03
WORKDIR /workspace
04
05
COPY .mvn .mvn
06
COPY mvnw pom.xml ./
07
RUN chmod +x mvnw && ./mvnw -DskipTests dependency:go-offline
08
09
COPY src src
10
RUN ./mvnw -DskipTests package
11
RUN java -Djarmode=tools \
12
-jar target/spring-0.0.1-SNAPSHOT.jar \
13
extract --layers --destination extracted
14
15
FROM eclipse-temurin:21-jre-jammy
16
17
RUN groupadd --system spring \
18
&& useradd --system --gid spring --home-dir /application spring
19
20
WORKDIR /application
21
22
COPY --from=builder --chown=spring:spring /workspace/extracted/dependencies/ ./
23
COPY --from=builder --chown=spring:spring /workspace/extracted/spring-boot-loader/ ./
24
COPY --from=builder --chown=spring:spring /workspace/extracted/snapshot-dependencies/ ./
25
COPY --from=builder --chown=spring:spring /workspace/extracted/application/ ./
26
27
USER spring:spring
28
29
EXPOSE 8080
30
ENTRYPOINT ["java", "-jar", "application.jar"]

第一个阶段使用完整 JDK 和 Maven Wrapper 编译项目;第二个阶段只保留 JRE 与运行文件,最终镜像不包含源码、编译器和 Maven 缓存。

Spring Boot 可执行 JAR 的 tools 模式把内容拆成稳定依赖、Boot Loader、快照依赖和应用代码。Docker 分别复制这些目录后,通常只有最上层应用代码会随日常修改失效,推送和拉取镜像时可以复用较大的依赖层。

USER spring:spring 让 Java 进程不以 root 身份运行。应用没有写入自身目录的需求,上传文件或临时数据以后应该使用明确挂载和权限,而不是重新切回 root。

EXPOSE 8080 只描述容器预期监听端口,不会自动把它发布到宿主机。真正的端口映射发生在 docker run -p 或 Compose 配置中。

4. 构建与运行

构建镜像并使用明确版本标签:

terminal.bash
1
docker build -t usehook/spring-health:0.1.0 .

运行容器时注入数据库连接和生产 Profile:

terminal.bash
1
docker run --rm \
2
--name spring-health \
3
-p 8080:8080 \
4
-e SPRING_PROFILES_ACTIVE=prod \
5
-e DB_URL='jdbc:postgresql://host.docker.internal:5432/spring' \
6
-e DB_USERNAME=spring \
7
-e DB_PASSWORD=spring \
8
usehook/spring-health:0.1.0

host.docker.internal 表示从 Docker Desktop 容器访问宿主机。它不适合表示另一个 Compose 容器,也不是所有 Linux 服务器都会默认提供这个名称。

验证业务与运维端点:

terminal.bash
1
curl http://localhost:8080/api/v1/health
2
curl http://localhost:8080/actuator/health/readiness

不要通过 docker exec 修改已经运行容器中的应用文件。需要修复代码时应构建新版本镜像,再替换容器,才能让部署状态可重现。

Spring Boot Maven Plugin 也能使用 Cloud Native Buildpacks 构建 OCI 镜像:

terminal.bash
1
./mvnw spring-boot:build-image \
2
-Dspring-boot.build-image.imageName=usehook/spring-health:0.1.0

Buildpacks 适合希望减少 Dockerfile 维护的项目;自定义 Dockerfile 则更直接地展示每个镜像层和运行用户。两种方式都应该产出不可变镜像,而不是把环境密码打包进去。

5. Compose 网络

数据库与应用都由 Compose 管理时,服务名就是容器网络中的主机名。把数据库章节的 compose.yaml 扩展为:

compose.yaml
01
name: spring-health
02
03
services:
04
app:
05
build: .
06
image: usehook/spring-health:0.1.0
07
environment:
08
SPRING_PROFILES_ACTIVE: prod
09
DB_URL: jdbc:postgresql://postgres:5432/spring
10
DB_USERNAME: spring
11
DB_PASSWORD: spring
12
ports:
13
- "8080:8080"
14
depends_on:
15
postgres:
16
condition: service_healthy
17
18
postgres:
19
image: postgres:17
20
environment:
21
POSTGRES_DB: spring
22
POSTGRES_USER: spring
23
POSTGRES_PASSWORD: spring
24
volumes:
25
- postgres_data:/var/lib/postgresql/data
26
healthcheck:
27
test: ["CMD-SHELL", "pg_isready -U spring -d spring"]
28
interval: 5s
29
timeout: 3s
30
retries: 10
31
32
volumes:
33
postgres_data:

应用使用 postgres:5432,因为 localhost 在 app 容器内部只表示 app 容器自己。PostgreSQL 没有发布宿主机端口,只有同一 Compose 网络中的服务可以直接访问它。

depends_on 的健康条件只控制启动顺序,不保证数据库以后永远可用。运行中断线仍然需要连接池超时、事务回滚、Readiness 和平台重试共同处理。

Compose 文件中的明文密码只适合当前本地案例。真实部署应使用平台 Secret、受限环境变量或外部秘密管理系统,并为应用账号授予最小数据库权限。

6. 发布边界

生产镜像还需要建立明确的供应链规则:

  • 使用可追踪版本或镜像摘要,不只依赖会移动的 latest;
  • 在构建流水线执行测试,构建完成后不再修改镜像;
  • 扫描基础镜像和 Maven 依赖漏洞,并按计划更新;
  • 为容器设置 CPU、内存和临时空间限制;
  • 把配置、证书和凭据放在运行环境,不写入镜像;
  • 将结构化日志写到标准输出,由平台采集;
  • 让 Actuator 管理端点只对受信网络开放。

容器内的 JVM 会感知资源限制,但堆内存、线程池、数据库连接池和平台副本数仍然要一起预算。数据库连接池章节计算的是全部实例的连接总量,不能只验证单个容器运行正常。

镜像构建成功只说明应用可以启动,还没有解决平台替换实例时如何停止流量、等待在途请求和验证新实例已真正就绪。下一篇继续学习优雅停机、启动任务、Readiness 状态与容器生命周期。