Docker Compose 部署 Spring Boot 项目
从多阶段镜像构建到 Compose 编排、健康检查、密钥注入和升级回滚,搭建可复现的单机生产部署基线。
把 Spring Boot 项目写进容器,只解决了“能启动”;生产部署还要回答镜像如何复现、数据库数据放在哪里、应用何时算就绪、密钥如何进入容器,以及升级失败如何退回。本文从一个 Spring Boot + PostgreSQL 服务出发,整理一套适用于单台 Linux 主机的 Compose 部署基线。它不是高可用集群方案,但能把构建与运行边界讲清楚。
先确定运行边界
应用容器应该是可替换的,持久化数据应该放在容器之外。对外只暴露反向代理或网关端口,应用与数据库留在 Compose 私有网络中;配置在部署时注入,镜像里不带环境密钥。
flowchart LR
client[浏览器 / API 客户端] -->|HTTPS :443| proxy[Nginx / 外部负载均衡]
proxy -->|私有网络 :8080| app[Spring Boot 容器]
app -->|JDBC| db[(PostgreSQL 容器)]
app --> logs[标准输出日志]
db --> volume[(命名卷 / 备份策略)]
secrets[部署密钥文件] -.只授权给 app.-> app
多阶段构建:构建环境不等于运行环境
以下 Dockerfile 用 Maven 构建 jar,再复制到精简 JRE 镜像。示例假设项目使用 Java 21 和 Spring Boot 可执行 jar;如果应用依赖字体、时区数据或原生库,需要在运行镜像中显式安装。
# syntax=docker/dockerfile:1
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /workspace
COPY pom.xml .
RUN mvn -B -q -DskipTests dependency:go-offline
COPY src ./src
RUN mvn -B -DskipTests package
FROM eclipse-temurin:21-jre
WORKDIR /app
RUN useradd --system --uid 10001 --create-home app
COPY --from=build --chown=app:app /workspace/target/*.jar /app/app.jar
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "/app/app.jar"]
dependency:go-offline 利用 Docker layer cache 复用依赖层;先复制 pom.xml 再复制源码,可以避免每次改一个 Java 文件都重新下载依赖。-DskipTests 只适合展示镜像构建步骤,CI 应在构建镜像前单独运行测试。不要把构建参数当作密码传给 ARG,镜像层和构建日志都可能留下痕迹。
同时准备 .dockerignore,让本地构建上下文保持小且不夹带敏感文件:
.git
.idea
target
*.log
.env
.env.*
secrets/
Compose:依赖顺序和就绪状态分开表达
服务“进程已经启动”不等于数据库已经能接受连接。Compose 的 depends_on 默认不代表业务就绪;应用需要自己的连接重试,Compose 则可以对带 healthcheck 的依赖等待 service_healthy。
name: leon-service
services:
app:
image: ${APP_IMAGE:-leon-service:local}
restart: unless-stopped
environment:
SPRING_PROFILES_ACTIVE: production
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/${POSTGRES_DB:-app}
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-app}
SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
MANAGEMENT_ENDPOINT_HEALTH_PROBES_ENABLED: "true"
expose:
- "8080"
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8080/actuator/health/readiness || exit 1"]
interval: 15s
timeout: 3s
retries: 5
start_period: 30s
networks: [backend]
db:
image: postgres:16.6
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-app}
POSTGRES_USER: ${POSTGRES_USER:-app}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 3s
retries: 5
start_period: 15s
networks: [backend]
networks:
backend:
volumes:
postgres_data:
这里假设运行镜像含 wget,并且 Spring Boot Actuator readiness probe 已启用;若使用 distroless 镜像,没有 shell/wget,就应改为镜像支持的健康检查工具或在宿主机/编排层探测。健康检查应测“能否接流量”,避免简单 TCP 连通性掩盖启动未完成。
配置与密钥
Compose 会从 shell 环境和 .env 文件解析 ${...},.env 主要是插值来源,不会自动把所有变量送进容器;显式列入 environment 的值才会注入。生产环境中可用受限权限的部署环境文件,或 Compose secrets。Compose secrets 在容器中以 /run/secrets/<name> 文件挂载,应用须支持从文件读取密码,不能假设它会自动成为环境变量。
services:
app:
secrets: [db_password]
db:
secrets: [db_password]
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
实际项目还要确认应用的密码读取方式与 PostgreSQL 镜像的 _FILE 约定相符;Compose secrets 本身解决的是按服务授权和文件挂载,不是集中式密钥保险库。密钥文件应限制文件权限、排除 Git、限制备份访问并制定轮换流程。
构建、迁移、启动与回滚
建议部署流程按可观察的步骤执行,而不是只写一个 docker compose up -d:
docker compose config # 展开并检查最终配置
docker compose pull # 拉取固定版本镜像
docker compose run --rm app java -version
docker compose up -d db
docker compose ps # 确认数据库健康
docker compose run --rm app java -jar /app/app.jar --spring.main.web-application-type=none
docker compose up -d app
docker compose logs --tail=100 -f app
迁移命令应替换成项目实际的 Flyway/Liquibase 策略。对大表变更,使用“先兼容旧版本,再部署新版本,最后清理旧字段”的 expand-and-contract 做法;不要让一个不可逆 schema migration 与应用升级绑成一次盲目重启。
升级前记录当前镜像 tag 或 digest,并保留上一版可拉取的镜像。出现问题时回到旧镜像只能回滚应用代码;数据库迁移若不兼容,仍需要前向修复或已演练的数据恢复。容器可重建不等于数据有备份,至少定期导出数据库,并实际演练恢复。
常见踩坑
- 把
localhost写成数据库地址:容器内的localhost指应用容器自己,服务间要用 Compose 服务名db。 - 只用
depends_on期待数据库就绪:顺序启动不保证 readiness;加 healthcheck,同时让应用具备连接重试。 - 把数据写在容器层:删除并重建容器后数据会丢。数据库文件应落在 volume,并做好独立备份。
- 用浮动
latest作为回滚点:部署时记录不可变 tag 或 digest,确保旧版本仍可获取。 - 健康检查依赖镜像里不存在的命令:最小镜像可能没有 curl/wget;构建时明确安装或选择适配的探测方式。
- 把
.env、证书或密钥打进镜像:检查.dockerignore和构建上下文,部署前用docker compose config核对插值结果,命令输出不要直接贴到公开日志。
上线前清单
- 应用以非 root 用户运行,镜像基于明确版本构建,并有漏洞扫描和来源记录。
- 只有入口代理公开端口;应用和数据库使用内部网络。
- readiness 与 liveness 语义清楚,日志写到 stdout/stderr,带请求关联 ID。
- 密钥按服务授权、限制文件权限并制定轮换;配置变更有审计记录。
- 数据库备份有保留策略,恢复步骤在隔离环境实际演练。
- 镜像升级、数据库迁移、失败回滚和前向修复都有可执行的操作步骤。