Docker Compose 避坑指南:10 个容易误判的问题
1. depends_on 排了顺序,数据库仍可能没准备好
下面这段配置只表达依赖关系:
services:
app:
build: .
depends_on:
- db
Compose 会先启动 db。但数据库进程刚启动时,可能还在初始化目录、恢复日志,暂时接不了连接。
可以给数据库加健康检查,再让应用等待检查通过:
services:
app:
build: .
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_DB: app
POSTGRES_PASSWORD: ${DB_PASSWORD:?请先设置 DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 10
start_period: 20s
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
这里的 $$ 是有意写的:让 Compose 保留美元符号,由容器内的 Shell 展开变量。写成 ${POSTGRES_USER},变量就会先在运行 Compose 的环境里求值。
这里显式用 -h 127.0.0.1 检查 TCP 连接,避免把官方镜像初始化期间只监听 Unix socket 的临时数据库当成已就绪。pg_isready 能判断服务是否接受连接,但不能证明账号鉴权成功、业务表已经建好。应用启动后的断线重连,仍然需要应用自己处理。
还有个容易漏掉的边界:在普通 Docker Engine / Compose 部署中,容器变成 unhealthy,不会仅因为这个状态就按 restart 策略重启。健康状态和进程退出是两回事。
2. .env 有值,容器里却没有
先分清两个位置:Compose 解析配置时使用的环境,和容器运行时的环境。
项目中的 .env 可以给 Compose 提供插值变量。它不会自动把所有变量塞进每个容器。假设 .env 写了:
APP_MODE=production
要让应用收到它,可以显式传入:
services:
app:
build: .
environment:
APP_MODE: ${APP_MODE:-development}
也可以通过服务的 env_file 批量传入文件中的变量:
services:
app:
build: .
env_file:
- ./app.env
命令行的 --env-file 为 Compose 插值提供变量;服务里的 env_file 配置容器环境。名字相近,用途不同。
值不对时,先检查是否被覆盖。Shell 中同名变量会优先于 .env 参与插值;同一服务同时配置 environment 和 env_file 时,environment 优先。
我更愿意让必填项在启动前就报错:
environment:
API_TOKEN: ${API_TOKEN:?请先设置 API_TOKEN}
LOG_LEVEL: ${LOG_LEVEL:-info}
DEBUG: "false"
DEBUG 这类值加上引号,避免被 YAML 当成布尔值。缺少令牌就直接退出,比让应用带着空值启动、等请求进来再失败好查。
涉及口令和令牌时,.env 只是文本文件。不要提交真实值,也别把完整环境变量截图贴进公开问题单;需要文件式传递的敏感信息,可以按应用支持情况使用 Compose secrets。
3. 改了配置,restart 后为什么还是旧值
docker compose restart 重启现有容器,不会重新应用 Compose 配置。环境变量、端口映射改了,光重启没有用。
根据改动选命令就清楚多了:
| 改了什么 | 通常怎么做 |
|---|---|
| Compose 中的环境变量、端口或挂载配置 | docker compose up -d |
| 通过 Dockerfile 打进镜像的源码,或 Dockerfile 本身 | docker compose up -d --build app |
| 需要获取远端仓库的新镜像 | 先 docker compose pull app,再 docker compose up -d app |
| 需要明确重建服务容器 | docker compose up -d --force-recreate app |
up 会在检测到服务配置或镜像变化时重建容器。--force-recreate 只强制重建容器,不等于重新构建镜像,也不等于拉取远端更新。
如果改的是挂载进容器的应用配置文件,文件内容可能已经更新,但进程是否自动重读,要看应用是否支持热加载。这时重启进程可能就够了,不能一概归到“必须重建”。
数据库初始化变量还要单独看。以 PostgreSQL 官方镜像为例,POSTGRES_PASSWORD 只在空数据目录初始化时设置口令;已有数据库不会因为重建容器就修改密码。要改已有账号的密码,需要在数据库里操作,再同步应用连接配置。
长期运行的服务可以按需要配置 restart: unless-stopped。不过,它不会修正错误的启动参数,也不会应用新配置;手动停止的容器,在 Docker 重启后仍会保持停止。
4. 容器里的 localhost,连的是谁
在默认 Compose 网络下,应用容器里的 localhost 指向应用容器自己。数据库跑在另一个容器里,地址应该用数据库的服务名。
假设服务名是 db,数据库监听容器内的 5432,端口发布配置为:
ports:
- "127.0.0.1:15432:5432"
连接地址要看客户端在哪儿:
| 客户端位置 | 数据库地址 |
|---|---|
| 同一 Compose 网络里的应用容器 | db:5432 |
| Docker 主机上的数据库客户端 | 127.0.0.1:15432 |
容器间通信使用容器端口,不需要绕到宿主机的 15432。也别把容器 IP 写死,重建后 IP 可能变化,服务名会保留。
如果数据库只供同一网络中的服务使用,可以不写 ports。需要本机调试,再显式绑定 127.0.0.1。直接写 "15432:5432" 时,默认会绑定所有主机接口;能否从外部访问,还取决于网络和访问控制。
5. down 和 down -v,差一个参数,数据处理就不同
普通的 docker compose down 默认删除项目容器和相关网络,保留具名数据卷。后面加上 -v,就会删除 Compose 文件中声明的具名卷,以及容器挂载的匿名卷;声明为 external 的卷不会被它删除。
所以,别把下面这条命令当成“重启得更彻底”:
# 会删除相应数据卷,仅在确认要清空这些数据时执行
docker compose down -v
数据库的数据目录要显式持久化。前面的 PostgreSQL 17 示例把具名卷 db-data 挂到 /var/lib/postgresql/data。PostgreSQL 官方镜像从 18 开始调整了默认目录布局和卷挂载位置,升级时要核对镜像说明和迁移要求,不能只改标签。
匿名卷也别忽略。它们在普通 down 后虽然可能还在,但后续 up 不会自动重新挂上原来的匿名卷。“卷没有删”和“新容器继续用原来的数据”并不是同一件事。
持久化解决的是容器替换后如何继续使用数据。误删表、写坏数据,照样会写进卷里。数据库备份和恢复验证还得单独做。
6. 换了个目录,数据库像被清空了
默认情况下,Compose 项目名来自项目目录名。通常,具名卷也会带上项目名前缀;显式设置卷的 name 或使用外部卷时,要按对应配置确认实际名称。
比如,同一份配置原来放在 blog 目录,后来搬到 blog-new,默认项目名随之改变。新的项目可能创建一个新卷,于是数据库又开始走初始化流程。旧数据未必丢了,先确认新容器到底挂了哪个卷。
可以在 Compose 文件中固定名称:
name: blog-prod
也可以每次都显式指定项目名:
docker compose -p blog-prod up -d
docker compose -p blog-prod ps
使用 -p 时,后续查看、更新和停止也要指向同一个项目。命令行 -p 的优先级高于 COMPOSE_PROJECT_NAME,后者又高于顶层 name。
开发、测试、生产要用不同项目名。宿主机端口和显式指定的外部资源还得另外区分,项目名不会替你隔离这些共享资源。
7. 配置文件没挂上去,宿主机却多了个目录
短语法看起来很省事:
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
但如果宿主机上的 nginx.conf 不存在,短语法的绑定挂载会在源路径创建目录。接下来可能报文件与目录类型不匹配,第一眼很容易怀疑到容器里的路径上。
文件挂载可以写得明确些:
volumes:
- type: bind
source: ./nginx.conf
target: /etc/nginx/nginx.conf
read_only: true
bind:
create_host_path: false
这样源文件缺失时会直接失败,不会顺手创建目录。
另一个常见现象是“镜像里明明有文件,容器启动后却找不到”。检查一下是否把宿主机目录挂到了同一位置。绑定挂载会遮住目标目录原有的内容,空目录挂到 /app,镜像里原来的 /app 文件也就暂时看不见了。
开发时挂载源码很方便。正式部署要保留这类挂载,就得确认宿主机目录里确实有应用需要的文件。
8. latest 让这次部署和上次部署对不上
latest 是一个标签。它不保证镜像稳定,也不能告诉你当前运行的是哪次构建。
我倾向于给业务镜像使用可追溯的发布标签,并让发布流程避免覆盖旧标签。数据库至少明确大版本,再按升级流程推进;需要严格复现时,可以锁定经过验证的 digest。
即使标签写得很具体,也不能从 Docker 的机制上保证它永远不变,发布者仍然可以覆盖标签。digest 才能明确指定镜像内容;锁定后也要安排更新,否则安全修复不会自己进来。
“仓库有新版”与“当前容器已经更新”之间还有拉取、重建容器这几个步骤。排查版本问题时,把镜像来源和容器实际使用的镜像一起核对,别只看 YAML 里的标签字符串。
9. 日志一直写,磁盘不会自己腾地方
如果使用 json-file 驱动,又没有配置轮转限制,日志文件默认没有大小上限。服务只要持续输出,磁盘占用就会继续增长。
可以在需要的服务中设置:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
这个配置限制 Docker 收集的标准输出和标准错误日志。应用自己写到 /app/logs 的文件,需要应用或其他日志方案负责轮转,不能指望上面的参数一起管住。
如果已经配置了别的日志驱动,就按对应驱动的规则检查保留策略。也别直接去 Docker 数据目录手动删日志文件,官方不建议绕过 Docker 操作这些文件。
10. 多个 Compose 文件,后一个不总是完整覆盖前一个
把通用配置放在 compose.yaml,再加一份环境配置,很常见:
docker compose -f compose.yaml -f compose.prod.yaml up -d
容易误判的是合并结果。后面的文件会根据字段规则覆盖、追加或合并,不能把它理解成“后一个文件写了什么,前一个就全部作废”。
例如,基础文件发布了 8080:80,生产文件又写了 80:80,合并后可能两个映射都保留。volumes 则按容器内的目标路径匹配。同一个服务里的不同字段,规则并不一样。
使用 -f 合并多个文件时,相对路径通常以第一个配置文件所在目录为基准。覆盖文件放在子目录里,也别直接按它自己的目录计算路径。
部署前先看最终结果:
docker compose -f compose.yaml -f compose.prod.yaml config
我会重点看端口、挂载源路径和环境变量。这几处只要与预期不一致,继续启动通常只会增加排查时间。
出问题时,先保留现场
服务异常时,先别急着 down。删除容器后,它的状态和本地容器日志也可能跟着没了。
先在正确的项目下检查:
docker compose config --quiet
docker compose ps -a
docker compose logs --tail=200 app db
config --quiet 用来校验配置,不会证明服务可用;ps -a 可以看到已退出的容器。日志里的第一条错误通常比后面反复出现的重试信息更有用。
怀疑变量替换或文件合并有问题,再看:
docker compose config
docker compose config --environment
前者展示解析、合并后的配置,后者展示参与插值的环境。输出可能含有口令和令牌,公开粘贴前先删掉敏感值。
如果启动命令用了 -p、-f 或 --env-file,排查时也带上相同参数,免得检查到另一套配置。先确认配置解析成了什么,再看容器状态、连接地址和挂载位置;确认原因之后,再决定该重启进程、重建容器,还是重新构建镜像。
- 点赞
- 收藏
- 关注作者
评论(0)