一、为什么需要Compose编排?从一次凌晨2点的故障说起
上周五晚上,我们一个内部管理系统突然无法登录。排查发现是后端服务先于数据库启动,连接池抛异常后进入死循环重启。这种问题在单机部署时很少遇到,但在容器化环境里,启动顺序和依赖检查成了必须处理的问题。
我们团队之前一直是手动docker run启动各服务,随着服务数量增加到6个,操作成本指数上升。而且不同服务的网络配置、环境变量、卷挂载分散在多个shell脚本里,没人能说清整体架构。这才下定决心迁移到Docker Compose统一编排。
本文不教基础语法,只分享一个可直接用于生产环境的三层应用编排方案,重点解决网络隔离、数据持久化、健康检查和启动顺序这四个核心痛点。
二、环境与版本说明
在开始之前,先明确我使用的环境版本(版本一致性是排查问题的第一步):
Docker Engine: 24.0.7
Docker Compose: v2.23.3
Linux Kernel: 5.15.0 (Ubuntu 22.04)
服务组件版本:
- Nginx: 1.25.3 (alpine镜像,体积仅23MB)
- Spring Boot: 3.1.5 (JDK 17,打包为可执行jar)
- PostgreSQL: 16.1 (使用官方镜像,内置健康检查脚本)
如果你的版本低于Docker Compose v2.20,部分healthcheck参数(如start_period)可能不支持,建议先升级。
三、方案设计:三层架构的网络拓扑
我们的应用是典型的前后端分离架构:Nginx作为反向代理处理静态资源和请求转发,Spring Boot提供REST API,PostgreSQL存储业务数据。
核心设计决策:
-
双网络隔离:创建两个自定义桥接网络——
frontend_net和backend_net。Nginx同时接入两个网络,但只有Nginx能访问后端服务,数据库完全隔离在外网,只对后端服务可见。这比默认桥接网络更安全。 -
命名卷挂载:PostgreSQL数据目录使用命名卷
pg_data,即使容器重建数据也不会丢失。后端日志目录挂载到宿主机./logs,方便排查问题。 -
健康检查三层递进:每个服务都配置healthcheck,但策略不同——数据库用
pg_isready命令,后端用Spring Boot Actuator的/actuator/health端点,Nginx检查/health路由的HTTP状态码。 -
启动顺序组合拳:光用
depends_on是不够的,必须配合condition: service_healthy。这样后端会等数据库健康后再启动,Nginx会等后端健康后再启动,形成严格的依赖链。
四、核心实现:完整docker-compose.yml配置
下面是完整的配置文件,我加了详细的注释说明每个参数的作用。请直接复制使用,但记得替换其中的环境变量为你的实际值。
version: "3.8"
# ========== 命名卷声明 ==========
volumes:
pg_data: # 数据库持久化卷
driver: local
nginx_logs: # Nginx日志卷
# ========== 自定义网络声明 ==========
networks:
frontend_net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24 # 显式指定子网,便于防火墙规则
backend_net:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/24
services:
# ---------- 1. PostgreSQL 数据库 ----------
db:
image: postgres:16.1-alpine
container_name: app-db
restart: unless-stopped
environment:
POSTGRES_DB: myapp
POSTGRES_USER: app_user
POSTGRES_PASSWORD: ${DB_PASSWORD} # 从.env文件读取,不要明文写入
volumes:
- pg_data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d:ro # 初始化SQL脚本
networks:
- backend_net # 只接入后端网络,不暴露到宿主机
ports:
- "127.0.0.1:5432:5432" # 仅本机可访问,用于开发调试
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d myapp"]
interval: 10s # 每10秒检查一次
timeout: 5s # 单次检查超时5秒
retries: 5 # 连续5次失败则标记为unhealthy
start_period: 30s # 容器启动后宽限30秒,避免启动初期误判
deploy:
resources:
limits:
memory: 1G # 限制内存防止OOM
# ---------- 2. Spring Boot 后端服务 ----------
backend:
build:
context: ./backend
dockerfile: Dockerfile
image: myapp-backend:1.0.0
container_name: app-backend
restart: unless-stopped
depends_on:
db:
condition: service_healthy # 关键:等待数据库健康后才启动
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/myapp
SPRING_DATASOURCE_USERNAME: app_user
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
SPRING_PROFILES_ACTIVE: prod
SERVER_PORT: 8080
volumes:
- ./logs:/app/logs # 挂载日志目录到宿主机
networks:
- backend_net # 只与数据库通信
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 40s # Spring Boot启动较慢,需要更长宽限时间
deploy:
resources:
limits:
memory: 1.5G
cpus: "1.0"
# ---------- 3. Nginx 反向代理 ----------
nginx:
image: nginx:1.25.3-alpine
container_name: app-nginx
restart: unless-stopped
depends_on:
backend:
condition: service_healthy # 确保后端已就绪
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- ./static:/usr/share/nginx/html:ro # 前端静态文件
- nginx_logs:/var/log/nginx
networks:
- frontend_net
- backend_net # 同时接入两个网络,作为网关
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost/health || exit 1"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10s
五、踩坑记录与优化方案
坑1:depends_on不是万能的
最初我只写了depends_on: - db,结果后端容器启动时数据库还在初始化,连接池报错。换成condition: service_healthy后问题解决。但注意:健康检查本身有延迟,所以start_period必须设置足够大。PostgreSQL首次初始化可能需要20-30秒,我设置了30秒。
坑2:healthcheck命令的选择
后端我用的是curl检查Actuator端点,但基础镜像(如eclipse-temurin)里没有curl。解决方案是在Dockerfile里安装curl,或者改用Java自带的JAVA_OPTS方式。我更推荐在基础镜像中预装curl,因为调试方便。Nginx镜像自带wget,所以用wget。
坑3:网络子网冲突
默认的bridge网络子网是172.17.0.0/16,如果宿主机已有VPN或虚拟网卡占用,会冲突。显式指定子网后彻底解决。
坑4:卷权限问题
PostgreSQL容器以postgres用户运行,但宿主机挂载目录权限可能不匹配。解决方案是使用命名卷(如pg_data),数据由容器内用户管理,宿主机只负责存储。不要挂载宿主机目录到数据库数据路径。
优化方案:
- 环境变量管理:使用
.env文件存储敏感信息,docker-compose自动读取。.env文件加入.gitignore。 - 日志轮转:Nginx和应用的日志如果不轮转,一个月能占满磁盘。在compose里加上
logging配置:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
六、效果数据与总结
迁移到Compose编排后,我们对部署流程做了计时对比:
| 指标 | 手动脚本部署 | Docker Compose |
|---|---|---|
| 部署时间 | 15分钟(含等待就绪) | 2分10秒 |
| 启动失败率 | 约12%(顺序问题) | 0.3% |
| 回滚时间 | 约8分钟(手动处理) | 30秒(docker compose down + up) |
| 服务可用性 | 99.2% | 99.95% |
核心收益: 配置即代码,整个应用架构在一个文件中清晰可见。新增开发环境只需复制compose文件并调整端口映射,团队成员不再需要读冗长的shell脚本。
几个建议:
1. 生产环境务必使用docker compose config校验配置正确性。
2. 结合docker compose scale横向扩展后端服务(但需要处理会话共享)。
3. 定期执行docker compose ps检查各服务健康状态,配合cron做自动告警。
最后说一句:容器编排不是银弹,但正确使用Compose能让你的部署流程从“玄学”变成“科学”。有问题欢迎在评论区交流。