目前应用可以通过 Maven 打包成可执行 JAR,也能从环境变量读取数据库和监控配置。生产部署还需要一份可重复的运行环境:明确 Java 版本、文件内容、启动命令和进程用户,并让同一镜像在不同环境接收不同配置。
本章把心跳应用制作成 Docker 镜像。我们会先区分 JAR 与镜像,再编写多阶段、分层 Dockerfile,最后让应用和 PostgreSQL 通过 Compose 网络协作。镜像只保存程序与运行时,密码仍在启动时注入。
1. JAR 与镜像
Maven 打包产生的 JAR 包含应用类、资源和依赖,运行它还需要操作系统中存在兼容的 Java:
1./mvnw clean package2java -jar target/spring-0.0.1-SNAPSHOT.jar
容器镜像在此基础上再固定运行时文件系统和启动命令:
1源代码 + pom.xml2-> Maven 构建3-> 可执行 JAR4-> Docker 构建5-> 应用镜像6-> 容器实例
镜像是只读模板,容器是镜像的一次运行实例。同一个镜像可以启动多个容器,每个实例通过环境变量、网络和资源限制获得不同运行配置。
不要在容器启动时重新编译源码。构建阶段应该产出已经通过测试的镜像,部署阶段只拉取这个不可变产物并提供环境配置,避免同一版本在不同机器重新解析出不同依赖。
2. 构建上下文
Docker 构建会把项目目录作为构建上下文发送给引擎。先创建 .dockerignore,排除无关或敏感内容:
1target2.git3.idea4.vscode5*.log6.env7compose*.yaml
target 会由镜像构建阶段重新产生,不需要从宿主机复制;.git 和编辑器目录只会增大上下文;.env 可能包含凭据,不应该进入镜像层。
.dockerignore 不是秘密管理工具。已经被 COPY 写入某个镜像层的文件,即使在后续层删除,也可能仍能从历史层恢复。敏感值从一开始就不应进入构建上下文和 Dockerfile。
为了让依赖层能够被缓存,Dockerfile 应先复制变化较少的 Maven 配置,再复制经常变化的源码。修改一个 Controller 时,Docker 就不必在每次构建中重新下载全部依赖。
3. 分层镜像
创建多阶段 Dockerfile:
01FROM eclipse-temurin:21-jdk-jammy AS builder0203WORKDIR /workspace0405COPY .mvn .mvn06COPY mvnw pom.xml ./07RUN chmod +x mvnw && ./mvnw -DskipTests dependency:go-offline0809COPY src src10RUN ./mvnw -DskipTests package11RUN java -Djarmode=tools \12-jar target/spring-0.0.1-SNAPSHOT.jar \13extract --layers --destination extracted1415FROM eclipse-temurin:21-jre-jammy1617RUN groupadd --system spring \18&& useradd --system --gid spring --home-dir /application spring1920WORKDIR /application2122COPY --from=builder --chown=spring:spring /workspace/extracted/dependencies/ ./23COPY --from=builder --chown=spring:spring /workspace/extracted/spring-boot-loader/ ./24COPY --from=builder --chown=spring:spring /workspace/extracted/snapshot-dependencies/ ./25COPY --from=builder --chown=spring:spring /workspace/extracted/application/ ./2627USER spring:spring2829EXPOSE 808030ENTRYPOINT ["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. 构建与运行
构建镜像并使用明确版本标签:
1docker build -t usehook/spring-health:0.1.0 .
运行容器时注入数据库连接和生产 Profile:
1docker 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 \8usehook/spring-health:0.1.0
host.docker.internal 表示从 Docker Desktop 容器访问宿主机。它不适合表示另一个 Compose 容器,也不是所有 Linux 服务器都会默认提供这个名称。
验证业务与运维端点:
1curl http://localhost:8080/api/v1/health2curl http://localhost:8080/actuator/health/readiness
不要通过 docker exec 修改已经运行容器中的应用文件。需要修复代码时应构建新版本镜像,再替换容器,才能让部署状态可重现。
Spring Boot Maven Plugin 也能使用 Cloud Native Buildpacks 构建 OCI 镜像:
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 扩展为:
01name: spring-health0203services:04app:05build: .06image: usehook/spring-health:0.1.007environment:08SPRING_PROFILES_ACTIVE: prod09DB_URL: jdbc:postgresql://postgres:5432/spring10DB_USERNAME: spring11DB_PASSWORD: spring12ports:13- "8080:8080"14depends_on:15postgres:16condition: service_healthy1718postgres:19image: postgres:1720environment:21POSTGRES_DB: spring22POSTGRES_USER: spring23POSTGRES_PASSWORD: spring24volumes:25- postgres_data:/var/lib/postgresql/data26healthcheck:27test: ["CMD-SHELL", "pg_isready -U spring -d spring"]28interval: 5s29timeout: 3s30retries: 103132volumes:33postgres_data:
应用使用 postgres:5432,因为 localhost 在 app 容器内部只表示 app 容器自己。PostgreSQL 没有发布宿主机端口,只有同一 Compose 网络中的服务可以直接访问它。
depends_on 的健康条件只控制启动顺序,不保证数据库以后永远可用。运行中断线仍然需要连接池超时、事务回滚、Readiness 和平台重试共同处理。
Compose 文件中的明文密码只适合当前本地案例。真实部署应使用平台 Secret、受限环境变量或外部秘密管理系统,并为应用账号授予最小数据库权限。
6. 发布边界
生产镜像还需要建立明确的供应链规则:
- 使用可追踪版本或镜像摘要,不只依赖会移动的
latest; - 在构建流水线执行测试,构建完成后不再修改镜像;
- 扫描基础镜像和 Maven 依赖漏洞,并按计划更新;
- 为容器设置 CPU、内存和临时空间限制;
- 把配置、证书和凭据放在运行环境,不写入镜像;
- 将结构化日志写到标准输出,由平台采集;
- 让 Actuator 管理端点只对受信网络开放。
容器内的 JVM 会感知资源限制,但堆内存、线程池、数据库连接池和平台副本数仍然要一起预算。数据库连接池章节计算的是全部实例的连接总量,不能只验证单个容器运行正常。
镜像构建成功只说明应用可以启动,还没有解决平台替换实例时如何停止流量、等待在途请求和验证新实例已真正就绪。下一篇继续学习优雅停机、启动任务、Readiness 状态与容器生命周期。