Docker Compose 避坑指南:10 个容易误判的问题

举报
yd_214609086 发表于 2026/10/23 17:04:45 2026/10/23
【摘要】 Docker Compose 配置没报错,服务却连不上;改了环境变量,重启后仍是旧值;换个目录,数据库像被清空了。本文结合配置示例和排查命令,讲清启动依赖、变量传递、容器网络、数据卷与更新流程中容易误判的地方。

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,排查时也带上相同参数,免得检查到另一套配置。先确认配置解析成了什么,再看容器状态、连接地址和挂载位置;确认原因之后,再决定该重启进程、重建容器,还是重新构建镜像。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。