Docker Compose 避坑指南③:上生产前必看,这 8 个坑每一个都能让你半夜被电话叫醒
本文是「DockerCompose避坑指南」系列的最后一篇,聚焦健康检查和生产部署中的关键问题。健康检查方面,详细讲解了命令写法、工具依赖验证等常见错误,并给出PostgreSQL、MySQL等常用服务的正确配置示例。生产部署部分涵盖镜像版本固定、日志限制、资源配额、非root用户运行、敏感信息管理等8个核心问题,每个问题都配有具体解决方案和代码示例。文章还提供了生产部署的完整Checklist和
系列说明: 这是「Docker Compose 避坑指南」三篇系列的最后一篇。前两篇讲了基础坑(YAML、依赖、环境变量)和进阶坑(网络、数据卷、多环境),本篇专攻健康检查和生产部署——这两块的坑不会在本地暴露,只会在生产环境的凌晨三点找上你。
前两篇的坑,绝大多数在开发阶段就能踩到、修掉。这一篇不一样。
健康检查写错了,服务会反复重启;镜像版本不固定,某天拉到不兼容版本直接挂;日志没有限制,磁盘被撑满;容器跑在 root 下,安全漏洞被利用……这些问题全都是「能跑很久没问题,但一旦出问题就是大问题」。
8 个坑,全是血泪经验。
环境说明
| 项目 | 版本 |
|---|---|
| Docker Engine | 24.x / 25.x / 26.x |
| Docker Compose | v2.x(docker compose 命令) |
| 操作系统 | Ubuntu 22.04 / CentOS 7·8 |
一、健康检查坑
坑 1:健康检查命令写错,容器永远 `unhealthy`
报错现象:
$ docker compose ps
NAME STATUS
db unhealthy
app unhealthy
依赖这些服务的容器无法启动,整个项目起不来。
常见错误写法:
healthcheck:
# 错误1:字符串形式但没用 CMD-SHELL,shell 语法不生效
test: "pg_isready -U postgres"
# 错误2:镜像内根本没有 curl
test: ["CMD", "curl", "http://localhost:8080/health"]
# 错误3:命令本身有问题,但没有去容器里手动验证过
test: ["CMD-SHELL", "mysql -u root -p${MYSQL_PASSWORD} -e 'SELECT 1'"]
根本原因:
健康检查命令在容器内部执行,依赖容器内已安装的工具。不同镜像内置的命令不同,Alpine 镜像普遍很精简,curl、bash 等工具默认不存在。
解决方案:
常用服务的正确健康检查写法:
services:
# PostgreSQL
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-postgres}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s # 给 PG 初始化留出时间
# MySQL
mysql:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# Redis
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"] # redis 镜像内有 redis-cli
interval: 10s
timeout: 3s
retries: 5
# Nginx
nginx:
image: nginx:alpine
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost/health || exit 1"] # alpine 有 wget
interval: 30s
timeout: 5s
retries: 3
# Node.js 应用(有 curl)
app:
image: myapp:latest
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s # 应用启动通常比数据库慢
不确定镜像里有没有某个命令,先进去确认:
docker compose exec service_name which curl
调试健康检查:
# 查看健康检查历史和失败原因
docker inspect --format='{{json .State.Health}}' container_name | jq .
# 手动执行健康检查命令测试
docker compose exec db pg_isready -U postgres
docker compose exec redis redis-cli ping
坑 2:健康检查通过了,但服务其实还没完全就绪
现象:
app 依赖 db,db 健康检查通过了,app 启动连接数据库,仍然失败。
根本原因:
健康检查只验证「进程在跑」,不代表「业务完全就绪」。比如:
- PostgreSQL
pg_isready返回成功,但数据库初始化脚本还没跑完 - MySQL 能 ping 通,但主从同步还没建立好
- 应用的
/health接口返回 200,但数据库连接池还没完成预热
解决方案一: 调整 start_period,给足启动宽限期:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 10
start_period: 60s # 宽限期内失败不计入 retries
解决方案二: 健康检查接口做真实的依赖验证:
// Node.js 示例:不只返回 200,真实检查依赖
app.get('/health', async (req, res) => {
const checks = {}
try {
await db.query('SELECT 1')
checks.database = 'ok'
} catch (err) {
checks.database = 'error: ' + err.message
}
try {
await redis.ping()
checks.redis = 'ok'
} catch (err) {
checks.redis = 'error: ' + err.message
}
const allOk = Object.values(checks).every(v => v === 'ok')
res.status(allOk ? 200 : 503).json({ status: allOk ? 'ok' : 'degraded', checks })
})
# Python Flask 示例
@app.route('/health')
def health():
try:
db.session.execute('SELECT 1')
redis_client.ping()
return jsonify({'status': 'ok'}), 200
except Exception as e:
return jsonify({'status': 'error', 'detail': str(e)}), 503
二、生产使用注意事项
坑 3:镜像版本不固定,某次更新把服务拉挂了
问题场景:
# ❌ 这三行配置埋了定时炸弹
image: redis:latest
image: nginx:latest
image: node:alpine
某天 docker compose pull 拉到了新版本,出现接口不兼容或行为变化,服务挂了。
根本原因:
latest 和不带版本的 alpine 标签,镜像内容随时会变,不具有可重复性。
解决方案:
生产环境固定到精确版本,最好精确到 patch 版本:
services:
redis:
image: redis:7.2.4-alpine
nginx:
image: nginx:1.25.4-alpine
db:
image: postgres:16.2-alpine
rabbitmq:
image: rabbitmq:3.12.12-management-alpine
自建服务使用语义版本 tag 或 Git commit SHA:
services:
app:
image: registry.example.com/myapp:${APP_VERSION}
# APP_VERSION=v1.2.3 或 APP_VERSION=abc1234
版本升级时,在测试环境验证后再更新生产配置,而不是靠 latest 自动漂移。
坑 4:容器日志无限增长,磁盘被撑满
报错现象:
No space left on device
/var/lib/docker/containers/ 目录占用了几十 GB。
根本原因:
Docker 默认日志驱动 json-file 没有大小限制,高频写日志的服务(如访问日志、慢查询日志)长期运行后会把磁盘吃满。
解决方案一: 全局配置(推荐),修改 Docker Daemon 默认日志限制:
// /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
# 修改后重启 Docker 生效(影响新创建的容器)
sudo systemctl restart docker
解决方案二: 在 Compose 文件中按服务配置:
services:
app:
logging:
driver: json-file
options:
max-size: "50m" # 单个日志文件最大 50MB
max-file: "5" # 保留最多 5 个轮转文件
nginx:
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"
接入集中日志系统(规模较大时推荐):
services:
app:
logging:
driver: loki
options:
loki-url: "http://loki:3100/loki/api/v1/push"
loki-batch-size: "400"
loki-retries: "3"
坑 5:不限制容器资源,单个服务把宿主机内存耗尽
报错现象:
宿主机 OOM,其他服务被系统强制 kill:
Killed
Out of memory: Kill process 12345 (java) score 900 or sacrifice child
根本原因:
容器默认可以使用宿主机的全部内存和 CPU,一个有内存泄漏的 Java 应用可以把整台机器的资源吃光。
解决方案:
为每个服务设置资源限制:
services:
app:
image: myapp:latest
deploy:
resources:
limits:
cpus: "1.5" # 最多 1.5 个 CPU 核
memory: 512M # 最多 512 MB 内存
reservations:
cpus: "0.25" # 预留 0.25 核(调度时保证)
memory: 256M
java-app:
image: myjavaapp:latest
environment:
# JVM 内存也要限制,不然 JVM 会按宿主机内存计算默认堆大小
JAVA_OPTS: "-Xms256m -Xmx512m -XX:MaxMetaspaceSize=128m"
deploy:
resources:
limits:
memory: 768M # 最大堆 512m + 元空间 128m + 其他,留余量
redis:
image: redis:7-alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
deploy:
resources:
limits:
memory: 384M
deploy.resources在docker compose命令下完全生效,不需要 Swarm 模式。
坑 6:容器以 root 用户运行,存在安全隐患
根本原因:
很多镜像默认以 root 用户运行进程。容器内的 root 虽然受到一定限制,但在某些漏洞场景下仍可能影响宿主机。这是容器安全的基本要求。
解决方案:
方式一:在 compose.yaml 直接指定 UID:
services:
app:
image: myapp:latest
user: "1000:1000" # UID:GID
nginx:
image: nginx:alpine
user: "101:101" # nginx 官方镜像的 nginx 用户 UID
方式二(更推荐):在 Dockerfile 里创建专用用户:
FROM node:20-alpine
# 创建非 root 用户
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --only=production
COPY --chown=appuser:appgroup . .
# 切换到非 root 用户
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
验证容器内实际运行用户:
docker compose exec app whoami
docker compose exec app id
坑 7:Secrets 只用 `.env` 文件,安全性不够
问题场景:
.env 文件以明文存储在服务器磁盘,任何有宿主机权限的人都能看到。而且:
# docker inspect 可以直接看到容器的环境变量明文
docker inspect container_name | grep -A 5 "Env"
解决方案:
使用 Compose secrets 机制,敏感数据通过文件形式注入,而非环境变量:
services:
db:
image: postgres:16
secrets:
- db_password
environment:
# 从文件读取,而不是直接的明文变量
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_USER: myapp
app:
image: myapp:latest
secrets:
- db_password
- jwt_secret
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
JWT_SECRET_FILE: /run/secrets/jwt_secret
secrets:
db_password:
file: ./secrets/db_password.txt # 开发用本地文件
jwt_secret:
file: ./secrets/jwt_secret.txt
# 创建 secrets 目录(不提交到 Git)
mkdir -p secrets
echo "your_strong_db_password" > secrets/db_password.txt
echo "your_jwt_secret_key" > secrets/jwt_secret.txt
chmod 600 secrets/*.txt
# .gitignore
secrets/
.env
应用代码读取 secret 文件:
// Node.js:从文件读取 secret
const fs = require('fs')
const dbPassword = process.env.DB_PASSWORD_FILE
? fs.readFileSync(process.env.DB_PASSWORD_FILE, 'utf8').trim()
: process.env.DB_PASSWORD
生产规模更大时,可以对接 HashiCorp Vault、AWS Secrets Manager、Azure Key Vault 等专业方案。
坑 8:`docker compose up` 不 rebuild,代码更新没有生效
根本原因:
如果 Compose 文件配置了 build,docker compose up 只在镜像不存在时才构建,不会检测代码变化。本地改了代码,重新 up 跑的还是旧镜像。
services:
app:
build: . # 有 build 配置
解决方案:
# 强制重新构建所有服务
docker compose up --build
# 只重新构建指定服务
docker compose build app
# 不用缓存构建(依赖包有更新时用)
docker compose build --no-cache app
# 构建并更新单个服务,不重启其他服务
docker compose build app && docker compose up -d --no-deps app
生产 CI/CD 标准流程:
#!/bin/bash
set -euo pipefail
APP_VERSION=$(git rev-parse --short HEAD)
# 1. 构建镜像,打上版本 tag
docker build -t registry.example.com/myapp:${APP_VERSION} .
docker build -t registry.example.com/myapp:latest .
# 2. 推送到镜像仓库
docker push registry.example.com/myapp:${APP_VERSION}
docker push registry.example.com/myapp:latest
# 3. 在生产服务器上拉取并更新
ssh prod-server "
export APP_VERSION=${APP_VERSION}
docker compose -f compose.yaml -f compose.prod.yaml pull app
docker compose -f compose.yaml -f compose.prod.yaml up -d --no-deps app
docker image prune -f # 清理旧镜像
"
生产部署完整 Checklist
健康检查
x所有服务配置了健康检查,且命令在容器内验证过可以正常执行
xstart_period 给了足够的启动宽限期
x应用的健康检查接口做了真实的依赖验证,不只是返回 200
镜像管理
x所有镜像固定到精确版本号,无 latest 或仅带大版本的 tag
x自建服务使用语义版本或 commit SHA 标识
日志
x配置了日志大小限制(max-size + max-file)
x或者接入了集中日志系统
资源限制
x所有服务设置了内存上限(deploy.resources.limits.memory)
xJava 服务同时设置了 JVM 堆内存参数
安全
x所有服务以非 root 用户运行,user 字段已配置
x数据库、Redis 等内部服务没有暴露端口到宿主机
x敏感信息通过 .env 或 Compose secrets 管理,不硬编码
部署
xCI/CD 脚本明确指定所有 -f 和 --env-file,不依赖隐式加载
x更新单个服务时使用 --no-deps,不影响其他正在运行的服务
x旧镜像定期清理,避免磁盘积累
常见问题 Q&A
Q1:docker compose up -d 之后如何确认所有服务都正常?
# 查看所有服务状态(含健康状态)
docker compose ps
# 实时跟踪日志(Ctrl+C 退出)
docker compose logs -f
# 只看某个服务的日志
docker compose logs -f app
# 查看最近 50 行
docker compose logs --tail=50 app
Q2:如何优雅地重启单个服务?
# 重启单个服务(不影响其他服务)
docker compose restart app
# 重新构建并更新单个服务(推荐)
docker compose up -d --no-deps --build app
# 查看重启后的状态
docker compose ps app
Q3:生产环境 Compose 和 Kubernetes 该怎么选?
| 考量维度 | Docker Compose | Kubernetes |
|---|---|---|
| 部署规模 | 单机或少数几台 | 多机集群 |
| 团队规模 | 小团队(< 10 人) | 中大型团队 |
| 运维复杂度 | 低,学习曲线平缓 | 高,需要专职运维 |
| 自动扩缩容 | 不支持 | 支持 |
| 高可用 | 需要额外方案 | 内置支持 |
| 适用场景 | 中小规模生产、内部工具 | 大规模、高可用需求 |
小团队用 Kubernetes 的运维成本往往远大于收益。Compose + 稳定的单机部署方案,完全能撑起日均百万级请求的服务。
Q4:容器内进程崩溃和整个容器退出有什么区别,怎么监控?
进程崩溃通常导致容器退出(退出码非 0),配合 restart: unless-stopped 容器会自动重启。
监控方式:
# 查看容器退出历史
docker compose ps -a
# 查看容器退出码
docker inspect container_name | grep ExitCode
# 配合监控工具(如 Prometheus + cAdvisor)实时采集容器状态
系列总结
三篇系列到这里结束,把覆盖的内容串一下:
第一篇(基础): YAML 语法(Tab、类型转换、多行字符串、锚点)+ 依赖控制(depends_on 本质、循环依赖、重启策略)+ 环境变量(密钥管理、优先级、多 env_file)
第二篇(进阶): 网络配置(localhost 误用、expose vs ports、网络隔离)+ 数据卷(权限、bind mount 滥用、down -v 风险)+ 多环境(覆盖机制、CI/CD 一致性)
第三篇(生产): 健康检查(命令写法、真实就绪检查)+ 生产注意事项(版本固定、日志限制、资源限制、非 root、secrets、镜像更新)
Docker Compose 本身并不复杂,坑大多来自「以为和预期一样,但其实不是」。把这 26 个坑过一遍,日常使用中 90% 的问题都能提前绕开。
更多《软件安装&避坑&实践》系列
公关众注号我:IT安装手册
更多推荐

所有评论(0)