Docker Compose 部署契约:Spring Boot、PostgreSQL、网络、健康检查与回滚
围绕 Spring Boot/Java + PostgreSQL 的 Compose 部署契约,解释服务名网络、健康检查、secrets、资源边界、备份恢复和更新回滚。
Spring Boot + PostgreSQL 的 Compose 部署,真正要管的不是“把容器跑起来”,而是把启动顺序、健康检查、网络、配置、密钥、资源、备份和回滚写成一份可执行契约。契约如果松散,容器化不会降低复杂度,只会把原本隐含的问题变成明确的发布事故。
这份中文正文只保留一条清晰的部署线:单机或小规模主机上,Java 服务、数据库和反向代理如何协同;什么时候该用 Compose,什么时候继续停在 VM 或裸机;什么时候不要把系统硬推到 Kubernetes。所有判断都围绕可复现、可回退、可验证这三个词展开。相关 Docker 文档可以查,但别把文档当成替代设计的借口。
如果要选一个统一入口去看官方资料,Docker 官方文档 里关于镜像、Compose、网络、卷和构建的说明已经足够覆盖大部分基础问题。生产时真正难的不是文档缺失,而是没有把文档落到你的部署契约里。
部署契约先定边界
| 层 | 契约内容 | 判断标准 |
|---|---|---|
| 镜像 | 编译后的 Spring Boot 产物和运行时依赖 | 是否固定版本、是否多阶段构建、是否可复现 |
| 容器 | 一个前台 Java 进程或一个 PostgreSQL 进程 | 是否能优雅停止、是否能健康检查 |
| 卷 | 数据库和需要持久化的数据 | 是否可独立备份、是否容器重建后仍存在 |
| 网络 | app、db、proxy 之间的内部互联 | 是否只用服务名,不依赖动态 IP |
| 配置 | 环境变量、env_file、secrets | 敏感信息是否脱离镜像和代码仓库 |
| 资源 | CPU、内存、日志和磁盘阈值 | 是否有边界,是否能被监控和告警 |
Spring Boot 适合放进 Compose 的前提,是它仍然保持单进程服务的姿态:启动、接收请求、访问数据库、优雅关闭。如果应用层本来就依赖大量本地状态、后台任务互相耦合、文件写入散落在容器可写层里,先整理应用,再考虑容器化。
Dockerfile 先做干净,再谈 Compose
Java 服务最常见的毛病,不是业务代码,而是 Dockerfile 写得过于随意:把构建环境和运行环境混在一起、镜像层数太多、容器以 root 运行、启动命令无法顺利接收停止信号。先用多阶段构建把边界拉开,后面的 Compose 才有意义。
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /workspace
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn -B dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn -B clean package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
RUN useradd --system --uid 10001 appuser
COPY --from=build /workspace/target/*.jar /app/app.jar
USER appuser
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app/app.jar"]
这份 Dockerfile 只做三件事:构建、复制、运行。最终镜像不带 Maven,不带源码,不带多余工具。这样做不是“更时髦”,而是把攻击面、故障面和回滚面都缩小到一条明确路径。
.dockerignore 决定构建上下文是否干净
.git
.idea
.vscode
target
build
*.log
.env
README.md
compose.override.yaml
node_modules
coverage
.DS_Store
构建上下文一旦脏,Docker build 就会更慢,缓存也更容易失真。更麻烦的是,密钥、日志和临时文件可能被无意带进上下文。.dockerignore 不是优化项,而是构建契约的一部分。
Compose 只负责单机协同,不负责替你修正应用
Compose 的作用是把 app、db、proxy 在同一台主机上排好。它不会替你修 Java 程序的端口监听,不会替你修数据库连接串,也不会替你补关闭逻辑。把该做的事情分清楚,部署就不会互相甩锅。
services:
app:
build: .
image: ideaicu/java-app:2026.07
environment:
SPRING_PROFILES_ACTIVE: prod
SERVER_PORT: 8080
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/app
SPRING_DATASOURCE_USERNAME: app
SPRING_DATASOURCE_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/actuator/health || exit 1"]
interval: 10s
timeout: 3s
retries: 5
stop_grace_period: 30s
init: true
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1.50'
memory: 1G
db:
image: postgres:16
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
proxy:
image: nginx:1.27-alpine
ports:
- "80:80"
- "443:443"
depends_on:
app:
condition: service_healthy
secrets:
db_password:
file: ./secrets/db_password.txt
volumes:
pgdata:
这份 Compose 里最关键的不是“能起”,而是每一层都能单独解释:app 只负责业务,db 只负责数据,proxy 只负责入口。服务名写死在内部网络里,不依赖动态 IP;数据库密码不进镜像,也不进 Git。
depends_on 只管顺序,不等于就绪
depends_on 的意义是让 Docker 先拉起依赖,再启动上层服务,但它不是健康保证。数据库进程起来以后,还要经历初始化、建表、恢复数据、装载参数。应用如果太早连库,报错通常不是 Compose 失败,而是就绪判断写得太乐观。
最小修复不是给 app 人工睡眠,而是把健康检查写成真实可用的判定:数据库用 pg_isready,应用用接口探针,反向代理用后端健康结果。顺序和就绪必须分开。
secrets、环境变量和配置文件要分层
密码、token、证书和私钥不要塞进镜像,不要硬编码进 Compose,也不要为了图省事写进仓库。可公开的配置放环境变量,敏感配置放 secrets,运行时需要的证书放只读挂载。这样做,回滚、迁移和权限审计都会简单很多。
数据库密码既可以给 PostgreSQL,也可以给 Spring Boot,只是传递方式不同。对于 Java 服务,若镜像里需要读取文件形式的 secret,可以用 _FILE 约定,减少把敏感值回流到环境变量的机会。
资源限制要提前写,不要等满了再补救
很多 Compose 项目刚上线时没事,一忙起来就开始把宿主机拖慢,原因不是 Docker 本身,而是资源边界没写。CPU、内存、磁盘和日志增长都应该是有上限的。Java 服务如果没有内存边界,JVM 可能把宿主机吃满;数据库如果没有卷空间预估,数据一涨就会卡住。
services:
app:
environment:
JAVA_TOOL_OPTIONS: -XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError
mem_limit: 1024m
db:
shm_size: 256m
mem_limit: 768m
资源限制不是要把服务勒紧,而是要把异常变成可见。超过上限时,系统应该更早报警,而不是在整机失去响应后才发现。数据库写满之前也要提前准备阈值,而不是等磁盘告急再找人处理。
反向代理是入口,不是装饰
公网流量最好经过反向代理,再进入应用容器。这样做的理由很朴素:TLS、压缩、缓存、限流、路径重写和访问控制都应该收束在边界层,不要散到业务代码里。应用容器只关心自己的接口,入口层再统一处理外部世界。
反向代理后面要确认转发头是稳定的,X-Forwarded-Proto、X-Forwarded-For 之类的头部别被漏掉,否则 Spring Boot 生成的回跳链接、跳转地址和安全重定向会乱。若应用依赖绝对 URL,这一点尤其重要。
日志要可读,也要可收敛
容器日志不是垃圾桶。Spring Boot 应该输出结构稳定的日志格式,错误要能直接定位到请求、线程和异常栈。Docker 这边则需要日志轮转或集中收集,避免单机日志把磁盘撑满。只要日志没有边界,后面的问题一定会回到磁盘。
docker compose logs -f app
docker logs --tail 200 compose-app-1
docker inspect compose-app-1 --format '{{.State.Health.Status}}'
排障时,日志要配合健康检查一起看。日志里报错不等于服务不可用,健康检查通过也不等于业务没有逻辑错误。二者结合,判断才稳。
备份、恢复和回滚不能混成一个动作
数据库备份是数据级动作,回滚是版本级动作,恢复是恢复演练。三者没有一个可以自动替代另一个。备份前要确认卷位置,恢复前要确认应用版本,回滚前要确认迁移是否可逆。只要其中一条不清楚,就不要把发布当成完成。
PostgreSQL 至少要准备卷级备份和逻辑备份两种思路:一类用于快速恢复,一类用于跨环境迁移和校验。应用升级前,先把迁移脚本和数据文件的方向想清楚,再决定是否允许自动更新。
更新回滚的最小流程
升级时先拉新镜像,切小流量或在维护窗口内切换,再观察健康检查、日志和数据库写入。发现异常后,立即回到上一版镜像标签和上一版 Compose 文件。不要在升级现场临时改三处配置再说“差不多好了”。
如果数据库迁移已经落下不可逆变更,回滚服务不等于回滚数据。这个边界要在上线前讲清楚,而不是等故障发生后再补定义。
安全边界不要靠习惯维持
容器安全不是把镜像换成更小就结束了。最少要管住三件事:非 root 运行、只读或最小写权限、敏感配置脱离镜像。能做的再补一层:基础镜像固定版本、文件权限收紧、必要端口最少暴露。
Compose 里如果把宿主机目录随意挂进去,安全和可移植性都会变差。挂载只留给真正需要持久化或共享的内容,其他东西让镜像自己承担,别把宿主机目录变成隐式依赖。
Docker 与虚拟机的边界
虚拟机解决的是完整操作系统隔离;Docker 解决的是进程级隔离和交付一致性。两者不是替代关系。若你需要的是不同内核、不同操作系统版本,或者强隔离边界,VM 更直接;若你要的是快速重建、统一打包和轻量协同,Compose 更合适。
因此,不要把 Docker 当成“小型虚拟机”,也不要把虚拟机当成“重一点的容器”。理解边界之后,很多设计就会变简单:数据库迁移适合卷和备份,入口流量适合代理,状态复杂的系统适合留在 VM 或独立主机上,而不是硬切到更复杂的编排层。
如果你需要一台长期跑 Compose 的单机主机,可以先看一眼 RainYun。部分链接可能包含返利,不影响推荐判断。真正该比较的是网络、磁盘、权限和运维边界是否适合这套契约。
什么时候不该上 Kubernetes
当系统规模还停留在单机或少量主机,且主要痛点仍然是 Dockerfile、.dockerignore、健康检查、依赖顺序、备份恢复和回滚时,继续把精力放在 Compose 上更划算。Kubernetes 解决的是更复杂的调度、滚动、弹性和多节点场景,它不会自动修好错误的连接串、错误的密钥管理或错误的停止逻辑。
如果部署还没有稳定到“能准确说清每一步契约”,就别急着把系统升到更重的编排层。先把这套单机契约做稳,再决定是否需要迁移。很多团队真正缺的不是 Kubernetes,而是把一个发布流程写清楚的耐心。
故障排查和命令速查
docker compose config
docker compose ps
docker compose logs -f app
docker compose up -d --build
docker compose down
docker system df
docker inspect <container>
docker exec -it <container> sh
docker volume ls
docker volume inspect pgdata
真正有效的排查顺序还是固定的:先看现象,再看证据,再做最小修复,最后验证和回滚。Compose 只是在这条线上提供协同工具,不会替代判断本身。把契约写清楚,问题就会少很多。
部署时最容易忽略的细节
第一,容器启动成功不代表接口已经 ready。第二,日志没有报错也不代表业务就绪。第三,数据库能够连通也不代表迁移已完成。第四,Compose 文件能被解析也不代表真正适合生产。这个四层分离一旦写在心里,很多现场判断就会稳一些。
此外,更新、备份、恢复和验证最好各自留痕。把今天的部署写进版本库、把回滚步骤写进 Runbook、把恢复脚本放进可执行目录,事故发生时就不会只剩口头经验。
Compose 上线前最后检查
- 应用是否固定端口,且健康探针能真实反映可用性。
- 数据库是否独立卷、独立备份、独立恢复。
- 环境变量是否只保留非敏感项,敏感值是否走 secrets。
- 反向代理是否处理了 TLS、转发头和入口限流。
- 资源限制是否已经写入,而不是等故障时口头补充。
- 回滚版本是否明确,迁移是否可逆已经被写下来。
命令与判断的关系
命令只是工具。真正让系统稳定的,是你能否把“看到什么现象,就去拿什么证据,再做什么最小修复”写成固定动作。只要这条动作链清楚,Compose 其实很好管;一旦动作链模糊,容器、网络、数据库和代理会一起变成噪音。
Docker 和虚拟机不是同一类工具
虚拟机把完整操作系统包起来,适合需要强隔离、不同内核或不同系统版本的场景;Docker 只包进进程、文件系统和依赖,更适合快速交付、一致运行和轻量协同。把这两个边界混淆,设计就会跑偏。
因此,若你的需求是“每个实例像一台独立机器”,VM 更合适;若你的需求是“一个应用加一个数据库,在同一台主机上稳定协同”,Compose 更合适。Docker 的价值不是替代 VM,而是在不需要完整系统隔离时,把交付成本压低。
什么时候该继续用 VM
- 你需要不同的内核能力或系统版本。
- 你要把风险边界切得更硬,审计要求更保守。
- 你不希望多个服务共享同一宿主机资源。
- 你需要把某些遗留系统维持在成熟的系统镜像里。
当这些条件成立时,继续用 VM 往往比容器化更清楚。把系统强塞进 Compose,不一定更简单,反而可能把依赖关系藏得更深。
Compose 里的备份和恢复要按可演练来设计
备份不是有个压缩包就算完成。能不能恢复、能不能验证、能不能在指定时间内回到可用状态,才是关键。PostgreSQL 的数据卷要定期做可验证备份,应用配置和密钥要有独立保存策略,恢复脚本要在隔离环境里真正跑通。
如果你从来没有做过恢复演练,那么所谓“有备份”只是心理安慰。恢复演练的目标不是追求完美,而是确认流程真的闭环。
恢复顺序建议
- 先恢复数据库卷或导入逻辑备份。
- 再检查 schema 与版本是否匹配。
- 然后启动应用,确认连接与迁移没有冲突。
- 最后切回入口,观察一段稳定窗口。
如果应用先起来、数据库后恢复,很多临时错误会让你误以为恢复失败。顺序本身就是排障的一部分。
资源边界写清楚,事故就少很多
Java 服务的内存、数据库的共享内存、日志的增长速度、宿主机磁盘空间,都应该有明确边界。没有边界的系统,平时看上去没事,一到高峰期就开始拖慢整机。Compose 不会自动替你做容量规划。
services:
app:
environment:
JAVA_TOOL_OPTIONS: -XX:MaxRAMPercentage=70 -XX:+ExitOnOutOfMemoryError
mem_limit: 1536m
db:
shm_size: 256m
mem_limit: 1024m
设置边界不是为了让服务更小气,而是为了让系统在接近极限时更早暴露问题。比起整机卡死,带着告警提前暴露,代价更低。
反向代理、日志和证书要协同看
如果入口层用了 Nginx、Traefik 或其他代理,就不能只看应用日志。代理的访问日志、错误日志和应用自身日志要一起看。证书续期、HTTP 到 HTTPS 跳转、真实客户端 IP、请求超时,这些问题常常卡在代理层,而不是 Java 进程本身。
如果代理层错了,应用可能完全没有异常,但用户已经进不来。反过来,应用错了,代理日志只会不断返回 5xx。两边一结合,故障轮廓就清楚了。
反向代理常见检查点
- 上游服务名是否写对。
- 监听端口和容器端口是否一致。
- 转发头是否保留。
- 证书和私钥是否独立管理。
- 超时配置是否和应用响应时间匹配。
安全不是附加项
最少权限运行、只读配置挂载、密钥和密码不进镜像、镜像标签固定、基础镜像来源可追踪,这些不是“加分项”,而是上线最基本的底线。安全边界没有写好,后续的优化都会建立在脆弱基础上。
尤其是数据库密码和第三方凭证,不要为了方便写进 Compose 文件,也不要为了省事塞进仓库。人可以知道配置放哪,但不该把秘密留在镜像层里。
命令速查要和判断一起记
真正有用的不是命令本身,而是命令背后的判断方向。比如 docker compose logs 是为了确认服务在说什么,docker inspect 是为了确认状态到底是什么,docker compose config 是为了确认最终渲染结果是不是你以为的那样。命令只是入口,结论还得靠证据。
如果你总是在命令之间跳来跳去,但说不清每一步想验证什么,那大概率还没把问题拆开。先想清楚要验证什么,再去敲命令,效率会高得多。
不要把 Kubernetes 当作补丁
当你还没有把 Dockerfile、多阶段构建、.dockerignore、depends_on、健康检查、优雅停止、备份恢复和回滚跑顺时,迁到 Kubernetes 不会让系统自动成熟。它只会把同样的错误放进更复杂的系统里。
真正该升级的不是平台,而是契约。把单机上的部署、数据和停止逻辑写稳,比盲目扩平台更划算。
部署检查表
- 镜像来源是否明确,版本是否固定。
- Dockerfile 是否分成构建与运行两段。
- .dockerignore 是否排除了无关和敏感文件。
- Compose 是否用 service name 连接服务。
- healthcheck 是否真的反映可用性。
- stop_grace_period 是否足够,应用是否能优雅退出。
- 卷、备份、恢复和回滚是否已经演练。
- 代理、日志、安全和资源边界是否都有明确配置。
最后的判断
只要这套契约能被写清、跑通、验证和回滚,Docker Compose 就能稳定承担大多数单机部署。它不需要被神化,也不该被草率替换。把边界画准,比追求更大的平台更重要。
Docker 的单机契约要写成可以读懂的说明
Compose 最怕的不是配置多,而是每一段都看似合理、连起来却没有边界。单机部署的契约至少要说清五件事:谁负责运行,谁负责存储,谁负责入口,谁负责配置,谁负责恢复。说不清,就意味着出了问题时没有统一的判断语言。
Spring Boot、PostgreSQL 和反向代理之所以常被放在一起,是因为它们的协作关系简单而明确:应用负责业务,数据库负责状态,代理负责入口。只要这三个角色互相越界,后续就会开始靠经验补洞。
角色划分要直白
- Spring Boot 只负责处理请求和调用数据库。
- PostgreSQL 只负责持久化、事务和数据恢复。
- 反向代理只负责接入、证书和流量转发。
- 宿主机只负责提供资源和系统能力。
- Compose 只负责把这些角色放在同一台主机上正确协同。
角色越清楚,排障越快。角色混了,问题就会被误判成“容器技术问题”。
多阶段构建要服务于可维护性
生产里的 Java 镜像最怕体积大、层次乱、权限高。多阶段构建的意义,是把“会变的构建环境”关在前一阶段,把“尽量少变的运行环境”留在后一阶段。这样镜像更小,问题也更容易定位。
如果构建阶段里还有测试、打包和依赖下载,那么运行阶段就不该再带这些工具。运行环境越干净,安全边界越清楚。
镜像里的最小集
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /workspace
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn -B dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn -B clean package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
RUN useradd --system --uid 10001 appuser
COPY --from=build /workspace/target/*.jar /app/app.jar
USER appuser
ENTRYPOINT ["java","-jar","/app/app.jar"]
这类镜像的好处,不只是省空间,还包括回退更直接。构建环境变了,不会污染运行环境;运行环境出错,也不会牵连编译工具。
.dockerignore 决定构建上下文的可信度
很多团队对 .dockerignore 的理解停留在“少传点文件”,其实它更像一份边界声明。哪些内容可以进入构建上下文,哪些内容必须留在上下文外,这本身就是可维护性的组成部分。
对于 Java 项目,target、build、日志、IDE 配置、临时覆盖文件都不该进入镜像构建流程。对前后端同仓、脚本混合仓库同样如此。
常见忽略项
.git
.idea
.vscode
target
build
coverage
*.log
.env
README.md
compose.override.yaml
node_modules
.DS_Store
如果不把这些东西先排除掉,构建缓存和安全边界都会变差。看上去只是慢一点,实际是可控性下降很多。
健康检查要覆盖真实可用性
应用能启动,不代表它能接真实流量。数据库能连上,不代表表结构和权限都对。代理能转发,不代表 TLS 和重定向都正确。健康检查必须尽量贴近真实可用状态,不能只验证进程存在。
建议把健康检查和 readiness 思路绑定起来:启动中、可用中、退场中,各自要有不同的信号。这样升级、回滚和故障判断才不会互相干扰。
services:
app:
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/actuator/health/readiness || exit 1"]
interval: 10s
timeout: 3s
retries: 5
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 10
如果没有 readiness 接口,就把它补出来。比起在 Compose 里堆等待时间,业务级健康判断更可靠。
依赖顺序和就绪状态要拆开看
depends_on 可以帮你把启动顺序排好,但它解决不了依赖服务初始化太慢的问题。数据库即使进程已经起来,也可能还在恢复数据、执行迁移、创建用户或装载参数。
最稳的做法,是把顺序交给 Compose,把就绪交给健康检查或应用重试。两个问题分开做,现场不会互相误导。
可读的启动关系
services:
app:
depends_on:
db:
condition: service_healthy
proxy:
depends_on:
app:
condition: service_healthy
这样写不是为了炫技,而是为了让人一眼看出谁依赖谁。依赖图清楚,排障就不会在多层日志之间来回跳。
secrets、证书与配置文件要分层
密码、token、证书和私钥不要塞进镜像,不要写死在环境变量里,更不要靠人工传递。公开配置和敏感配置必须分层,这样迁移、审计和回滚才不互相牵连。
如果应用支持 _FILE 方式读取密钥,就尽量用文件形式;如果不支持,也应该通过最小权限的挂载目录来隔离。别把秘密放进镜像层。
配置放置原则
- 公开参数放环境变量。
- 敏感值放 secrets。
- 证书和私钥放只读挂载。
- 运行时生成的临时文件不要放进持久卷。
这四条看起来老生常谈,但一旦做实,后面的维护成本会低很多。
资源和磁盘边界要提前约束
Java 的内存控制、PostgreSQL 的共享内存、日志增长和磁盘空间,最好都在上线前写清楚。否则平时看不出问题,忙起来就开始拖慢宿主机。Compose 不会自动帮你做容量规划。
services:
app:
environment:
JAVA_TOOL_OPTIONS: -XX:MaxRAMPercentage=70 -XX:+ExitOnOutOfMemoryError
mem_limit: 1536m
db:
shm_size: 256m
mem_limit: 1024m
资源限制的作用不是让服务“更小”,而是让问题更早显形。早点报警,总代价更低。
反向代理、日志和入口治理
入口层是边界,不是摆设。Nginx、Traefik 或其他代理在前面时,TLS、重定向、压缩、限流和真实客户端 IP 都应该由入口层统一处理,而不是散落在应用里。
应用日志和代理日志要一起看。代理层 502、应用层 500、数据库层连接失败,三者的症状会长得很像,但定位方向完全不同。
代理层要确认的点
- 上游服务名是否稳定。
- 证书是否与域名匹配。
- 转发头是否保留。
- 超时是否与应用响应匹配。
- 是否给错误响应留出足够日志。
备份、恢复和演练应该一体化
数据库卷的备份、应用配置的备份和恢复脚本,最好放在一起管理,但不要混成一个文件。恢复演练必须真的跑过一次,不然“有备份”只是主观感觉。
恢复顺序建议固定下来:先恢复数据库,再检查 schema,再启动应用,再切流量。把顺序写在 Runbook 里,现场会轻松很多。
恢复演练的最小闭环
- 恢复到隔离环境。
- 检查关键表和关键数据。
- 确认应用版本与数据版本兼容。
- 再切回入口并观察稳定窗口。
没有闭环的备份,不算可用备份。
优雅停止和更新回滚要成对设计
服务更新时,不该只关注拉起新版本,也要关注旧版本如何平稳退出。SIGTERM、stop_grace_period、关闭钩子、事务完成和连接池回收,最好一起设计。
如果一个版本不能优雅停掉,那它也很难可靠回滚,因为旧版本和新版本都可能被挂在半空中。
services:
app:
stop_signal: SIGTERM
stop_grace_period: 45s
init: true
restart: unless-stopped
回滚时先退应用,再退入口,再检查数据。不要一口气把所有东西都改掉,然后期待系统自动自愈。
虚拟机和 Docker 的边界要看清
VM 提供的是完整操作系统级别的隔离,Docker 提供的是进程和交付级别的一致性。前者更硬,后者更快。若需求是强隔离、系统版本差异或遗留环境稳定运行,VM 往往更合适;若需求是快速交付、统一运行和单机协同,Compose 更合适。
所以不要拿 Docker 替代所有 VM 场景,也不要把 VM 当成更慢的 Compose。工具不同,边界也不同。
如果你需要一台长期跑 Compose 的单机主机,可以先看一眼 RainYun。部分链接可能包含返利,不影响推荐判断。重点还是主机、磁盘、网络和权限是否适合这套契约。
什么时候别把系统推到 Kubernetes
如果主要问题还在 Dockerfile、.dockerignore、健康检查、依赖顺序、备份恢复和停止逻辑这些基础层面,就先别把精力放到 Kubernetes。更重的编排层不会自动修好契约,也不会自动补上运维纪律。
当你的部署还没有稳定到能清楚写出“谁负责什么、出错怎么退、数据怎么保”,迁移到 Kubernetes 往往只是把旧问题换个平台重演。
命令速查与判断提示
docker compose config
docker compose ps
docker compose logs -f app
docker compose up -d --build
docker compose down
docker system df
docker inspect <container>
docker exec -it <container> sh
docker volume ls
docker volume inspect pgdata
命令只是把证据摆出来。真正决定成败的是你是否能把现象、证据、最小修复、验证和回滚连成一条线。部署契约写清楚,排障和维护都会轻很多。
把发布动作拆开,别让一次发布承担太多责任
Spring Boot + PostgreSQL 的部署,最容易出问题的地方不是“容器跑不起来”,而是一次发布里塞了太多动作:镜像更新、配置变更、数据库迁移、代理切流、备份切换全挤在一起。这样做一旦出错,根本不知道是哪一步先偏了。
更稳的方式,是把动作拆开:先确认新镜像可拉起,再确认健康检查可通过,再确认数据库迁移可完成,最后才切流。任何一步有问题,就停在那一步,不要向前滚动。
推荐的发布顺序
- 构建并固定镜像版本。
- 在预发或隔离环境里启动容器。
- 确认健康检查、日志和数据库连接都正常。
- 执行迁移和备份验证。
- 最后切换入口流量。
这套顺序看上去保守,但它能明显减少线上事故的复杂度。发布不是比谁动作快,而是比谁动作可控。
回滚和恢复不要混用
回滚是把服务退回旧版本,恢复是把数据恢复到可用状态。两个动作往往同时出现,但绝不相同。服务可以回滚,数据未必能回滚;数据能恢复,服务未必能直接回到旧配置。
如果数据库迁移已经写入不可逆变更,那你能回滚的只是应用层,不是全部系统。这个边界上线前就要讲清楚,不然故障来了以后会产生错误预期。
回滚前的最小检查
- 旧镜像是否存在且可拉取。
- 旧环境变量和 secrets 是否仍然有效。
- 数据库迁移是否可逆,或者是否有反向脚本。
- 反向代理规则是否保留了旧入口。
- 备份是否覆盖了当前业务窗口。
有了这些检查,回滚才是按预案执行,而不是现场赌博。
Compose 环境里最常见的责任错位
有些问题看似是 Docker 问题,其实是职责写错了。应用把数据写到容器可写层,是应用错位;数据库没有卷,是存储错位;反向代理没有处理 HTTPS,是入口错位;把 secrets 放进镜像,是安全错位。错位比故障本身更难看见。
把职责写得更清楚以后,故障会减少很多,因为每个层都知道自己该做什么、不该做什么。
为什么这类文章要强调真实边界
很多 Docker 文章喜欢把一切都说得很顺,但生产现场恰恰相反:边界不清,问题就会变成一串误判。把容器、镜像、卷、网络、停止、回滚、备份、代理和 Kubernetes 边界说透,才是真的对排障有帮助。
如果你能在现场快速说清“现象是什么、证据在哪、只改哪一处、怎么验证、怎么退回去”,那这套系统就已经比大多数混乱部署稳得多了。
命令速查最好配一个解释列
很多速查表只列命令,不写用途,结果看到命令还是不知道什么时候该用。更好的做法,是把命令和目的绑在一起:看状态用什么,看日志用什么,看配置用什么,看卷用什么,看资源用什么。命令不是为了炫技,是为了加速判断。
docker compose config # 看最终渲染结果
docker compose ps # 看服务状态
docker compose logs -f app # 看应用日志
docker inspect app # 看容器细节
docker volume inspect pgdata # 看卷位置和挂载
一张好用的速查表,应该让人拿到以后就能直接定位问题,而不是继续猜。
不是所有系统都适合继续容器化
如果系统本身就是一个重量级遗留应用,依赖奇怪的本地状态、复杂的文件共享和大量手工操作,那么强行把它塞进 Compose 也未必有收益。容器化最擅长的是把边界清楚的服务标准化,而不是替你重写历史包袱。
所以,决定要不要上 Docker,不该只看“能不能跑”,还要看“跑起来之后是否更清楚”。如果更不清楚,那可能就不值得。
更新时最重要的是可观察性
更新不是把旧版本换成新版本就结束了。你还得能看见切换是否成功、数据库有没有异常、代理有没有 5xx、日志有没有重复重连、用户有没有明显失败。没有可观察性,更新就是盲动。
在生产里,多给自己一点观察窗口,往往比一味追求快更安全。只要指标、日志和接口状态都明确,更新就能稳很多。
如果要迁移到更复杂平台,先补齐这几项
在考虑 Kubernetes 之前,先确认你已经把这几项做稳:镜像可复现、配置可追踪、健康检查可信、数据库有备份、回滚可执行、停止可优雅、入口有代理、资源有边界。没有这些,迁移只会把问题放大。
这不是反对迁移,而是反对在基础不稳时迁移。平台越重,错误的代价越大。
最后再强调一次部署契约
Compose 的价值,是把 Spring Boot、PostgreSQL、代理、配置和回滚放进一份能读懂、能执行、能复盘的契约里。只要契约清楚,单机部署就能稳;契约不清楚,哪怕换成更大的平台,问题还是会回来。
边界越清楚,Compose 越好维护
维护一个 Compose 项目,最难的不是写配置,而是持续守住边界。每次新增服务、改端口、加密钥、调资源、动代理,都应该回到同一套规则:谁负责什么、失败怎么退、数据放哪里、流量从哪进。只要边界一致,项目就不会越来越散。
最容易失控的是“顺手改一下”。顺手加一个挂载、顺手写一个环境变量、顺手改一个 healthcheck,单独看都不大,叠起来就会把契约弄乱。所以改动前最好先问一句:这是不是在改变边界?
改动前先问四个问题
- 这个改动是在服务内部,还是在服务边界?
- 这个改动会不会影响回滚?
- 这个改动会不会影响备份或恢复?
- 这个改动会不会让健康检查失真?
如果其中任一答案不清楚,就应该先暂停,而不是先提交。
什么时候更适合留在单机上
不是所有系统都值得马上上更复杂平台。若服务数量不多、数据路径简单、团队规模有限,单机 Compose 往往比更重的编排更容易维护。你会更清楚看到每一个容器、每一条网络、每一个卷,也更容易在故障时定位问题。
单机不是落后,而是边界更少、判断更直接。只要主机资源和业务范围都能承载,Compose 其实是很好的中间层。
把恢复能力写进日常流程
恢复能力不是事故来了才临时准备,而是平时就应该能练。定期验证备份、定期检查卷、定期试跑恢复流程,比平时只看绿灯更有意义。恢复跑不通,说明系统的可恢复性还没有真正成立。
很多团队只有“上线流程”,没有“恢复流程”。这两个流程同样重要。上线是把系统带到生产,恢复是把系统带回可用。少了后者,前者就不完整。
把管理动作固化成最小闭环
一个能长期维护的 Compose 项目,应该有自己的最小闭环:构建、验证、发布、观察、回滚、恢复。每个动作都能独立执行,且互相不缠绕。闭环越小,出问题时越容易定位。
如果一次改动同时触碰了镜像、数据库、代理和资源限制,那就不是一个普通改动,而是一次需要更严谨验证的变更。把变更拆开,是为了让回滚也能拆开。
最小闭环清单
- 构建镜像并固定版本号。
- 在隔离环境里运行并检查健康状态。
- 确认数据库可用、代理可达、日志正常。
- 完成发布后观察一段稳定窗口。
- 必要时按预案回滚并复核数据。
如果这五步能跑通,很多维护问题就会少很多。
把文档、脚本和配置放在一起管理
配置文件、回滚脚本、备份脚本、恢复步骤和命令速查,最好都在同一个维护目录里,而不是散在聊天记录和个人笔记里。这样即使不是原作者接手,也能继续维护。
文档不是摆设,它是把经验变成流程的载体。流程能落地,项目才算真的可交接。
日志归档和备份演练要一起做
日志归档和备份演练通常被分开安排,但它们其实应该一起做。日志能告诉你某个时刻系统的行为,备份演练能告诉你某个时刻的数据能不能回来。两者合起来,才能证明系统真的可恢复。
如果只有备份、没有恢复演练,或者只有日志、没有归档和保留策略,那系统的历史就不完整。生产里最怕的是“出了事才发现证据没了”。
建议的保留思路
- 应用日志按天轮转,保留近期故障窗口。
- 数据库备份按日或按业务窗口保留。
- 恢复演练至少定期跑一次隔离环境。
- 关键发布时保留镜像标签、Compose 文件和版本号。
这样做的目的不是为了显得流程复杂,而是为了让每一次故障都留下足够证据。
Docker 与虚拟机的边界:先把概念拆开
虚拟机给你的是完整 guest OS,Docker 给你的是进程级隔离和文件系统、网络、权限边界。排障时如果把容器当成一台小服务器,就会把主机层、镜像层和应用层混在一起,最后很难判断问题到底出在哪一层。
一个简单的学习示例就够用了:先用 docker run 跑出最小容器,再看它如何退出、如何接收信号、如何挂载数据、如何映射端口。真正能回退的部署,不是“跑起来就行”,而是能把镜像内容、运行参数和数据位置都说清楚。
docker run --rm alpine:3.21 echo ok
DIGEST=$(docker image inspect --format '{{index .RepoDigests 0}}' alpine:3.21)
docker run --rm "$DIGEST" echo ok
这里的重点是 digest 固定。tag 适合学习和试跑,digest 适合生产回退和审计。你如果只记得镜像名,不记得镜像内容,更新后出问题时就很难证明自己回到了同一份软件。
Volume、Bind Mount 和 --mount:把数据放对地方
数据库、上传文件和需要长期保留的状态,应该放在 Volume 里,而不是放进容器可写层。Bind Mount 更适合配置文件、证据导出和明确知道宿主机路径的场景。--mount 比短参数更清楚,尤其适合排障时把挂载边界说透。
docker run -d --name pg-demo -v pgdata:/var/lib/postgresql/data postgres:16
docker run --rm -it --mount type=bind,src=/srv/app/conf,dst=/etc/app,readonly alpine cat /etc/app/app.yaml
docker run --rm --mount type=volume,src=pgdata,dst=/var/lib/postgresql/data,readonly alpine ls -la /var/lib/postgresql/data
只读挂载的作用不是“更高级”,而是防止你在排障时顺手改坏现场。先把数据读出来、拍下来、比对出来,再决定是否恢复或重建。
PostgreSQL 备份恢复:卷不是备份
很多人看到数据库卷还在,就以为备份也在。其实 Volume 只说明数据路径存在,不说明你能回到某个时间点。真正的恢复能力要靠 pg_dump 和 pg_restore 这样的逻辑备份/恢复流程来证明。
docker exec -t pgdb pg_dump -U app -Fc appdb > appdb.dump
docker exec -i pgdb pg_restore -U app -d appdb --clean < appdb.dump
如果是生产数据库,最好再补一个隔离环境恢复验证:备份文件可读、对象可恢复、连接串可用、业务表能查。没有恢复演练的备份,只是占磁盘的文件。
安全边界:privileged、/var/run/docker.sock、非 root 和端口暴露
--privileged 不是“临时加大权限”这么简单,它会把很多宿主机能力一起放进容器。把 /var/run/docker.sock 暴露给容器也一样危险,因为那几乎等于把 Docker 控制权交给容器内进程。
docker run --privileged --rm alpine sh
docker run -v /var/run/docker.sock:/var/run/docker.sock --rm alpine sh
docker run --rm -p 127.0.0.1:8080:8080 --user 10001:10001 myapp:1.2.3
更安全的默认值是非 root 运行、最小端口暴露、只把必要端口交给反向代理,其他入口交给防火墙、云安全组或主机规则收口。不要把“容器里开了端口”误解成“外网就应该能访问”。
对应的官方文档
- Docker Engine 官方安装文档:先把引擎装对,再谈部署。
- Dockerfile 最佳实践:镜像层、缓存和构建上下文的官方建议。
- Volumes 文档:理解持久化数据边界。
- Compose 启动顺序说明:判断 depends_on 的实际能力边界。
- Engine 安全文档:privileged、daemon 暴露和最小权限都要看这里。
- Rootless 模式:了解非 root 运行方式。
- Compose 参考手册:字段语义和配置语法以它为准。