1. 问题背景:为什么需要精细化编排?
去年接手一个电商后台项目,架构很标准:前端Nginx反向代理,后端Spring Boot应用,数据层PostgreSQL。开发时各服务用docker run手动启动,一到测试环境就出问题:
- 启动竞态:Spring Boot启动时PostgreSQL还没就绪,导致连接超时失败,需要手动重启两次才能成功。
- 网络混乱:所有容器都在默认bridge网络里,Nginx能直接连数据库,存在安全隐患。
- 卷权限:PostgreSQL挂载宿主机目录后,启动时因权限不足写不了数据,容器反复重启。
- 日志丢失:Spring Boot容器重启后日志随之消失,排查线上问题极其困难。
这些问题的根因是缺乏标准化的容器编排方案。Docker Compose能一次性解决,但如果你只是简单定义services,该踩的坑一个不少。下面我会展示一个生产可用版本,包含网络隔离、健康检查、启动顺序和卷挂载。
2. 环境与版本
- Docker版本:24.0.7
- Docker Compose版本:v2.24.1(注意:v1已弃用,请使用docker compose命令)
- 镜像:
- nginx:1.25.3-alpine(Alpine基础镜像,体积小,约25MB)
- postgres:16.1-alpine(支持健康检查的官方镜像)
- 业务镜像:registry.example.com/order-service:2.3.1(基于Eclipse Temurin 17 JRE,构建后约180MB)
宿主机操作系统:Ubuntu 22.04 LTS,内核5.15.0-91。
3. 方案设计:网络、卷与启动策略
整体架构分为三层:
- 外部访问层:Nginx容器暴露80端口,反向代理到后端。
- 业务逻辑层:Spring Boot容器,仅对Nginx暴露,不直接对外。
- 数据层:PostgreSQL容器,完全隔离在内部网络,不对外暴露任何端口。
网络策略:
- 创建两个自定义网络:frontend(Nginx与Spring Boot通信)和backend(Spring Boot与PostgreSQL通信)。
- Spring Boot容器同时加入两个网络,作为桥梁。Nginx只加入frontend,PostgreSQL只加入backend。
卷挂载策略:
- PostgreSQL:挂载pgdata卷到/var/lib/postgresql/data,持久化数据库文件。
- Spring Boot:挂载logs卷到/app/logs,防止日志随容器销毁。
- Nginx:挂载宿主机配置文件目录,方便热更新配置。
启动顺序:
- 使用depends_on定义依赖关系,但仅靠depends_on只能保证容器启动,不能保证服务就绪。
- 关键:在depends_on基础上添加condition: service_healthy,配合healthcheck实现真正就绪检测。
4. 核心实现:可运行的docker-compose.yml
直接上配置,完整的docker-compose.yml如下:
version: '3.8'
networks:
frontend:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
backend:
driver: bridge
internal: true # 禁止外部访问,增强安全性
ipam:
config:
- subnet: 172.20.1.0/24
volumes:
pgdata:
driver: local
driver_opts:
type: none
o: bind
device: /data/postgres # 生产环境建议用独立磁盘分区
logs:
driver: local
driver_opts:
type: none
o: bind
device: /data/app-logs
services:
postgres:
image: postgres:16.1-alpine
container_name: order-db
networks:
- backend
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: order_db
POSTGRES_USER: order_user
POSTGRES_PASSWORD: ${DB_PASSWORD} # 从.env文件读取,勿硬编码
healthcheck:
test: ["CMD-SHELL", "pg_isready -U order_user -d order_db"]
interval: 5s
timeout: 3s
retries: 5
start_period: 30s # 给数据库初始化留出时间
restart: unless-stopped
deploy:
resources:
limits:
memory: 512M
backend:
image: registry.example.com/order-service:2.3.1
container_name: order-backend
depends_on:
postgres:
condition: service_healthy
networks:
- frontend
- backend
volumes:
- logs:/app/logs
- ./config/application.yml:/app/config/application.yml:ro # 外部配置文件
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/order_db
SPRING_DATASOURCE_USERNAME: order_user
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
SERVER_PORT: 8080
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 60s # Spring Boot启动较慢,给足时间
restart: unless-stopped
deploy:
resources:
limits:
memory: 1G
nginx:
image: nginx:1.25.3-alpine
container_name: order-nginx
depends_on:
backend:
condition: service_healthy
networks:
- frontend
ports:
- "80:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 30s
timeout: 5s
retries: 2
restart: unless-stopped
deploy:
resources:
limits:
memory: 128M
关键配置解释:
networks.backend.internal: true:这个参数很多人忽略。设为true后,该网络内的容器无法访问外部网络,只能与同网络容器通信。PostgreSQL只暴露给后端服务,即使宿主机被攻破,攻击者也无法直接连接数据库。depends_on+condition: service_healthy:这是解决启动顺序的核心。例如backend会等待postgres容器通过健康检查后才启动自己的容器。注意:condition: service_healthy在Docker Compose v2中才完全支持,确保docker compose version>= 2.20。start_period:健康检查的"预热期"。在此时间内,健康检查失败不计入重试次数,避免因启动慢导致误判。PostgreSQL设为30s,Spring Boot设为60s,实测足够。- 卷挂载使用bind mount:生产环境建议用
driver_opts绑定宿主机的特定目录,而不是用匿名卷。方便运维备份和排查。
Nginx配置(./nginx/conf.d/default.conf)示例:
upstream backend {
server backend:8080;
}
server {
listen 80;
server_name order.example.com;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
5. 踩坑与优化
5.1 权限踩坑:PostgreSQL启动失败
第一次启动时,PostgreSQL报错:FATAL: could not create directory "/var/lib/postgresql/data/pgdata": Permission denied。原因是我用root用户挂载了宿主机的/data/postgres目录,但PostgreSQL容器内使用uid 999(postgres用户)运行,没有写入权限。
解决方案:在宿主机上设置目录权限:
sudo mkdir -p /data/postgres
sudo chown 999:999 /data/postgres # 999是postgres用户uid
或者在docker-compose.yml中通过user指令指定容器运行用户,但推荐前一种方式,保持镜像原始用户。
5.2 健康检查超时:Spring Boot启动慢
Spring Boot应用启动需要约45秒(加载JDBC连接池、初始化Flyway迁移),但健康检查默认interval: 30s,导致第一次检查时应用还没就绪,被标记为不健康。虽然start_period可以缓解,但注意start_period不计数重试,如果应用启动时间超过start_period,后续健康检查失败依然会触发重启。
优化:将start_period设为60s,interval设为10s,retries设为3。这样应用启动后最多30秒内就能通过健康检查。
5.3 网络性能:internal网络的好处
一开始我把所有服务放在同一个自定义网络中,发现Nginx到后端的延迟偶尔飙升到200ms。排查发现是Nginx容器和PostgreSQL容器在同一网络,虽然流量不大,但Docker DNS解析和网络包转发有干扰。拆分网络后,延迟稳定在1-2ms。
internal: true还有一个隐藏好处:PostgreSQL容器无法发起出站连接,即使被注入恶意代码也无法外传数据,符合安全合规要求。
6. 效果数据
上线这个编排方案后,对比之前的裸docker run部署:
| 指标 | 之前 | 之后 |
|---|---|---|
| 服务启动成功率 | 73% | 99.2% |
| 平均部署时间 | 3分20秒 | 1分58秒 |
| 日志找回成功率 | 0%(重启即丢失) | 100% |
| 数据库连接错误 | 每次部署必现 | 0次(连续30天) |
其中启动成功率提升的关键是健康检查与depends_on的配合。之前哪怕手动等待数据库启动再启动后端,也经常因为网络闪断导致失败。现在condition: service_healthy确保数据库真正可查询后才启动后端,同时后端的健康检查又卡住了Nginx,形成严格的启动链。
日志持久化带来的另一个收益:配合logrotate对/data/app-logs进行每日切割,现在可以回溯7天内的日志,排查问题时直接grep宿主机目录,不用再进容器。
7. 总结
Docker Compose编排不是简单地把多个docker run命令拼在一起。要想在生产环境中稳定运行,必须处理三个核心问题:
- 网络隔离:用多个自定义网络按流量方向划分,
internal: true给数据库等敏感服务加锁。 - 启动顺序:
depends_on+condition: service_healthy是黄金组合,配合start_period避免假阳性。 - 数据持久化:bind mount到宿主机指定目录,并管理好容器用户权限。
最后一点建议:永远不要在docker-compose.yml里硬编码密码等敏感信息。使用环境变量文件.env,并在.gitignore中排除它。上面配置中的${DB_PASSWORD}就是从.env读取的。
希望这篇博客能帮你少踩几个坑。如果你的服务架构更复杂(比如消息队列、Redis集群),后续我会分享如何用Docker Compose编排有状态服务。评论区欢迎交流。