首页/文章/Docker / DevOps
Docker / DevOps

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,并保留上一版可拉取的镜像。出现问题时回到旧镜像只能回滚应用代码;数据库迁移若不兼容,仍需要前向修复或已演练的数据恢复。容器可重建不等于数据有备份,至少定期导出数据库,并实际演练恢复。

常见踩坑

  1. 把 localhost 写成数据库地址:容器内的 localhost 指应用容器自己,服务间要用 Compose 服务名 db。
  2. 只用 depends_on 期待数据库就绪:顺序启动不保证 readiness;加 healthcheck,同时让应用具备连接重试。
  3. 把数据写在容器层:删除并重建容器后数据会丢。数据库文件应落在 volume,并做好独立备份。
  4. 用浮动 latest 作为回滚点:部署时记录不可变 tag 或 digest,确保旧版本仍可获取。
  5. 健康检查依赖镜像里不存在的命令:最小镜像可能没有 curl/wget;构建时明确安装或选择适配的探测方式。
  6. 把 .env、证书或密钥打进镜像:检查 .dockerignore 和构建上下文,部署前用 docker compose config 核对插值结果,命令输出不要直接贴到公开日志。

上线前清单

  • 应用以非 root 用户运行,镜像基于明确版本构建,并有漏洞扫描和来源记录。
  • 只有入口代理公开端口;应用和数据库使用内部网络。
  • readiness 与 liveness 语义清楚,日志写到 stdout/stderr,带请求关联 ID。
  • 密钥按服务授权、限制文件权限并制定轮换;配置变更有审计记录。
  • 数据库备份有保留策略,恢复步骤在隔离环境实际演练。
  • 镜像升级、数据库迁移、失败回滚和前向修复都有可执行的操作步骤。

参考资料

文章反馈

如果这篇文章对你有帮助,欢迎发邮件交流。

通过邮箱联系 ↗继续阅读 →