Docker Compose 生产实战——多服务编排、健康检查、启动顺序控制
Docker Compose 生产实战——多服务编排、健康检查、启动顺序控制
适读人群:用 Docker Compose 管理多服务的工程师,想让 compose 跑得更稳的人 | 阅读时长:约 14 分钟 | 核心价值:生产级 compose 配置的完整写法,避开高频踩坑
很多团队把 Docker Compose 当"开发环境工具",觉得生产环境要用 K8s。这个观点不完全对——对于中小规模的项目,单机 Docker Compose 的可靠性其实相当高,前提是你把配置写对了。
我见过太多 compose 配置"看起来能跑,但在生产上出问题":MySQL 还没准备好,应用就启动了;Redis 重启后应用不自动重连;某个服务崩溃了,compose 没有自动拉起……这些问题都可以通过正确的配置解决。
这篇文章把生产级 Docker Compose 配置的几个关键点讲透。
从一个"看起来正常"的 compose 文件说起
下面这个配置,很多人会认为没问题:
version: '3'
services:
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
app:
image: myapp:latest
depends_on:
- db
ports:
- "8080:80"实际上有三个严重问题:
depends_on只控制容器启动顺序,不保证 PostgreSQL 服务可用——容器启动了,但数据库进程可能还在初始化- 没有健康检查,compose 不知道服务是否真正健康
- 没有重启策略,服务崩溃后不会自动恢复
踩坑实录一:depends_on 不等于"等数据库准备好"
这是 Docker Compose 最经典的误解。
现象:应用启动报错 connection refused,日志显示数据库连接失败,但明明加了 depends_on: - db。
原因:depends_on 只是让 compose 先启动 db 容器,再启动 app 容器。但"容器启动"和"数据库服务可接受连接"之间有一段时间差,PostgreSQL 初始化可能需要 3~8 秒。
解法一(推荐):在应用层做重试,这是更优雅的解法:
# Python 示例:连接重试
import time
import psycopg2
from psycopg2 import OperationalError
def create_connection(max_retries=10, retry_interval=2):
for attempt in range(max_retries):
try:
conn = psycopg2.connect(
host=os.getenv("DB_HOST"),
port=os.getenv("DB_PORT", 5432),
database=os.getenv("DB_NAME"),
user=os.getenv("DB_USER"),
password=os.getenv("DB_PASSWORD"),
)
print(f"Database connected on attempt {attempt + 1}")
return conn
except OperationalError as e:
if attempt < max_retries - 1:
print(f"Connection attempt {attempt + 1} failed, retrying in {retry_interval}s...")
time.sleep(retry_interval)
else:
raise解法二:在 compose 里用健康检查 + condition: service_healthy:
services:
db:
image: postgres:15-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s # 给 PostgreSQL 足够的初始化时间
app:
image: myapp:latest
depends_on:
db:
condition: service_healthy # 等待 db 健康检查通过!condition: service_healthy 是关键——这才是真正等待服务就绪的写法。
完整的生产级 compose 配置
直接给一个我们在用的完整配置,包含 Web 应用、数据库、缓存、反向代理:
# docker-compose.prod.yml
version: '3.8'
# 统一管理密钥,不要在 environment 里硬写密码
secrets:
db_password:
file: ./secrets/db_password.txt
redis_password:
file: ./secrets/redis_password.txt
# 持久化数据卷
volumes:
postgres_data:
driver: local
redis_data:
driver: local
nginx_logs:
driver: local
# 内部网络,服务间通信不暴露到宿主机
networks:
backend:
driver: bridge
internal: true # 完全隔离,容器无法访问外网
frontend:
driver: bridge
services:
# ── 数据库 ──────────────────────────────────────
postgres:
image: postgres:15-alpine
restart: unless-stopped
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d:ro
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp -d myapp"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# 资源限制,防止单个服务吃满宿主机
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
reservations:
memory: 256M
# ── 缓存 ──────────────────────────────────────
redis:
image: redis:7-alpine
restart: unless-stopped
command: >
redis-server
--requirepass-file /run/secrets/redis_password
--maxmemory 256mb
--maxmemory-policy allkeys-lru
--save 60 1
--loglevel warning
volumes:
- redis_data:/data
secrets:
- redis_password
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
deploy:
resources:
limits:
memory: 300M
# ── 应用 ──────────────────────────────────────
app:
image: myapp:${APP_VERSION:-latest}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
NODE_ENV: production
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: myapp
DB_USER: myapp
REDIS_HOST: redis
REDIS_PORT: 6379
secrets:
- db_password
- redis_password
networks:
- backend
- frontend
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval: 15s
timeout: 5s
retries: 3
start_period: 20s
deploy:
resources:
limits:
memory: 512M
cpus: '2.0'
# 日志配置,避免日志撑满磁盘
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
# ── 反向代理 ──────────────────────────────────
nginx:
image: nginx:1.25-alpine
restart: unless-stopped
depends_on:
app:
condition: service_healthy
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./ssl:/etc/nginx/ssl:ro
- nginx_logs:/var/log/nginx
networks:
- frontend
healthcheck:
test: ["CMD-SHELL", "nginx -t && curl -f http://localhost/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"踩坑实录二:restart: always 导致无法停止服务
现象:执行 docker compose stop,服务确实停了,但几秒钟后又自动起来了。
原因:restart: always 在任何情况下都重启,包括手动停止。这在某些场景下会很烦人,比如你想临时维护某个服务。
解法:改用 restart: unless-stopped。这个策略的行为是:除非你明确执行了 docker stop 或 docker compose stop,才不会重启;其他情况(容器崩溃、Docker daemon 重启后)都会自动重启。
| 重启策略 | 说明 |
|---|---|
no | 永不重启 |
always | 任何情况都重启,手动停止后 docker 重启也会拉起 |
unless-stopped | 除手动停止外都重启,推荐 |
on-failure[:max] | 仅在非零退出码时重启,可限制次数 |
踩坑实录三:compose 配置里硬写密码被 git 提交
现象:同事代码 review 的时候发现 compose 文件里有明文数据库密码,已经提交到了仓库历史里。
原因:图省事在 environment 里直接写了 POSTGRES_PASSWORD: my_secret_pass。
解法:三种方案:
方案一:.env 文件 + .gitignore(最简单)
# .env 文件(加入 .gitignore,不提交)
POSTGRES_PASSWORD=my_actual_password
# compose 文件里引用
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}方案二:Docker secrets(更安全,我在生产用这个)
# 创建 secret 文件(只存本地,不提交 git)
mkdir -p secrets
echo "my_actual_password" > secrets/db_password.txt
echo "secrets/" >> .gitignore然后在 compose 里用 secrets 声明,如上面完整配置所示。
方案三:外部密钥管理(适合大团队) 集成 Vault、AWS Secrets Manager 等,容器启动时动态获取密钥。
健康检查编写要点
健康检查写得不好,反而会帮倒忙。几个注意点:
healthcheck:
# 检查命令要轻量,不能比服务本身还耗资源
test: ["CMD-SHELL", "curl -sf http://localhost:8080/actuator/health | grep UP || exit 1"]
# interval: 两次检查之间的间隔
interval: 15s
# timeout: 单次检查的超时
timeout: 5s
# retries: 连续失败多少次才判定为 unhealthy
retries: 3
# start_period: 容器启动后多久开始算检查失败(给应用初始化时间)
start_period: 30sstart_period 非常重要——如果你的应用启动需要 20 秒(比如 Spring Boot 加载慢),但 retries=3 且 interval=5s,那 15 秒内就会被判定 unhealthy,此时容器还在正常启动,却已经被标记为不健康了。
日常运维命令
# 生产环境启动(后台)
docker compose -f docker-compose.prod.yml up -d
# 滚动更新某个服务(先拉镜像,再重建)
APP_VERSION=v2.3.1 docker compose -f docker-compose.prod.yml up -d --no-deps app
# 查看服务状态和健康情况
docker compose ps
# 查看实时日志(最近 100 行开始)
docker compose logs -f --tail=100 app
# 在运行中的容器内执行命令
docker compose exec app sh
# 查看资源使用情况
docker stats $(docker compose ps -q)
# 完全停止并删除(保留 volume)
docker compose down
# 停止并删除,包括 volume(慎用!会删数据)
docker compose down -vDocker Compose 做生产部署其实挺靠谱,关键在于把健康检查、重启策略、密钥管理这几个点配置到位。很多人用了好几年 compose,却从来没写过 healthcheck——这才是风险最大的地方。
