ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深入理解Docker Compose:架构原理与实战排障指南

深入理解Docker Compose:架构原理与实战排障指南 1. 为什么需要 Compose从 docker run 的失控说起我印象特别深的一个项目早期部署一个微服务应用里面只有四个组件——后端 API、Redis、MySQL、Nginx。最初大家都是直接docker run每个服务一条命令环境变量、端口映射、数据卷、网络参数全都堆在命令行里。单个服务看起来还好最多是命令长一点等到要更新版本、换环境、或者从一台机器搬到另一台机器时问题就集中爆发了。最典型的场景是这样的某次服务器重启后我需要在干净的机器上把整套服务拉起来先得按顺序启动 MySQL等它初始化完成再启动 Redis然后等后端服务健康检查通过最后才能把 Nginx 挂上去。因为容器之间有依赖关系纯靠docker run就得手写一套启动脚本 等待逻辑脚本里全是sleep 10、重试循环、状态判断。更要命的是这套脚本跟具体机器绑得很死——换一台部署机IP、路径、配置全变了脚本基本作废。这其实就是 Docker 本身解决不了的问题。Docker 只解决了单个容器的打包与运行但多个容器如何组织成一个应用系统它并不管。谁来定义容器之间的依赖关系谁来保证服务启动的先后顺序谁来统一管理网络、数据卷、环境变量谁来让整套系统在开发机、测试机、生产机上保持一致Docker Compose 就是奔着这些问题来的。它的核心思路其实特别朴素把一套多容器应用的状态用一份 YAML 文件描述出来然后用一个命令把这份描述变成现实。这份 YAML 文件就是docker-compose.yml它声明了什么Compose 就创建什么服务列表、镜像来源、端口映射、数据卷挂载、网络连接、环境变量、依赖关系全部集中在这份文件里。我说句实在话在 Kubernetes 流行之前Docker Compose 基本是中小型项目编排多容器的默认答案。即使是现在K8s 已经满大街都是Compose 在本地开发、单机部署、CI 集成这些场景里依然不可替代因为它足够轻、足够简单、足够贴近我们只是想把几个容器跑起来这个真实需求。要真正用好 Compose光会写几个docker compose up -d是不够的。你必须理解它背后的架构模型和工作原理——它究竟是怎么解析配置的、怎么创建容器的、怎么管理网络的、怎么处理数据卷的这些才是遇到问题时能快速定位的关键。这篇文章就把 Compose 的核心原理和架构拆开聊一遍顺便把部署中间件时经常踩到的坑一并说清楚。2. 三层抽象模型Project、Service 与 ContainerCompose 架构上最核心的东西是一套三层抽象模型。如果不理解这三层你很难把 Compose 的行为看透为什么docker compose ps看到的容器名字那么长为什么重名目录会导致容器冲突为什么明明配置了container_name就不能水平扩展这些疑问全都藏在这三层里。2.1 第一层Project项目Project 是 Compose 中最高层级的抽象。一个 Project 代表一组逻辑上属于同一个应用的容器集合。默认情况下项目名就是 docker-compose.yml 所在目录的目录名你也可以通过-p参数或环境变量COMPOSE_PROJECT_NAME显式指定。Project 的作用体现在两个地方。第一它是资源隔离的边界同一个 Project 下的容器共享同一个网络、共享同一个卷命名前缀不同 Project 之间的资源默认互相隔离即使两个项目里都定义了 MySQL 服务只要项目名不同它们的容器名、网络名、卷名都不会冲突。第二它是编排的单元所有对容器组的批量操作启动、停止、删除都是以 Project 为粒度执行的。我在实际项目里就遇到过一种典型问题同一个应用在两台机器上用不同目录名部署结果容器名、网络名都跟着变了导致我们的一套监控脚本写的固定容器名在另一台机器上报错。后来统一用-p appname指定项目名这个问题才彻底解决。所以这里有个很重要的经验不要把项目名交给目录名自动决定在环境敏感的场景里显式指定项目名永远是更稳妥的做法。2.2 第二层Service服务Service 是docker-compose.yml里services段下的每一个顶层键。它描述的是某种容器的模板用什么镜像、多少副本、什么启动命令、映射哪些端口、挂载哪些卷、依赖哪些其他服务。Service 和 Container 的区别容易被忽略但必须搞清楚Service 是想法的定义Container 是想法的实例。一个 Service 在docker compose up后可能对应多个容器实例最常见的就是docker compose up --scale api3这种水平扩展场景一个名为 api 的 Service 会创建 3 个容器。如果给 Service 显式设置了container_name就意味着这个 Service 只能对应一个容器--scale也就没法用了。Service 抽象的价值在于它让 Compose 可以像描述规格一样描述运行。你不需要关心容器具体叫什么名字、落在哪个 IP 上只需要告诉 Compose这个服务需要 3 份副本、每个副本映射宿主机的 8080 端口、挂载同一份数据卷。剩下的事情 Compose 来处理。2.3 第三层Container容器Container 是最底层、最具体的实例。Compose 在创建容器时会自动命名规则是项目名-服务名-序号比如myapp-api-1、myapp-redis-1。这个序号是 Compose 在 Project 范围内维护的递增编号用来区分同一 Service 下的不同实例。理解了这一点就不会再困惑为什么我明明没指定容器名容器却被命名成app-web-1这种奇奇怪怪的样子。容器这一层还牵扯到一个关键机制标签Label。Compose 在创建每个容器时会自动打上一系列标签比如com.docker.compose.projectmyapp、com.docker.compose.serviceweb、com.docker.compose.container-number1等等。这一组标签是整个 Compose 架构的暗线——它让 Compose 能在成千上万个容器里精准识别哪些容器属于我管。docker compose ps为什么不需要传容器名就能列出当前项目的容器因为 Compose 内部就是用项目名去查com.docker.compose.projectproject这个标签的容器列表。同理docker compose stop、docker compose rm这些命令也都是靠标签定位目标容器。如果你想手动操作 Compose 管理的容器或者用挂外部工具做集成保留这些标签非常重要。我自己就吃过亏在手动清理容器时用了docker rm -f删除一个 Compose 管理的容器结果后面执行docker compose up时Compose 检测到容器缺失和实际状态不一致需要手动重建反而多花了几分钟。实际操作中还有一点容易忽略container_name一旦设置容器名就固定了不再遵守项目名-服务名-序号规则。这在大多数场景下没问题但如果你的部署脚本里有按容器名正则匹配的习惯设置container_name后一定要同步更新脚本否则很容易漏掉容器。下面这张表可以帮你快速理解三层模型在命令行层面的映射关系抽象层Compose 中的体现Docker CLI 的对应生命周期命令Project项目名目录名 /-p参数无直接对应up、down、ps 作用于整个 ProjectServiceYAML 中 services 段下的键类似 image 定义 多个 run 参数的组合scale、run 针对单个 ServiceContainer实际运行的容器进程docker run/create的产物stop、rm、logs 针对单个 Container3. 一次 compose up 命令背后的事件链很多人对 Compose 的认识停留在写一个 YAML 然后跑 up但真正发生火灾时能救你的是对事件链的了解。我把docker compose up -d背后做的事情拆开来看你会发现它本质上是一个配置解析器 一个差异计算器 一个 Docker API 调用器的组合。3.1 配置解析与归一化当你在项目目录中执行docker compose up时Compose CLI 做的第一件事是读取docker-compose.yml。但这不只是一个简单的 YAML 加载过程它会做一层很关键的归一化Normalization。比如ports段你可以写短语法8080:80也可以写长语法target: 80, published: 8080, protocol: tcp。volumes段你可以写短语法./data:/var/lib/mysql也可以写完整的 bind 挂载描述。Compose 会把所有短语法统一展开成规范化的内部结构再交给下一步处理。为什么需要这一步因为后续的容器创建、网络配置、标签标注都需要统一的、无歧义的参数。可以说Compose 的内部本质上有一份规范化的配置模型用户写的 YAML 只是它的一个表达形式。这也就是 compose 文件格式Compose Specification中version字段存在意义减弱的原因。早期版本里配置模型和解析器的行为差异很大version用来选择解析器的兼容模式到了 Compose v2 时代解析器已经基本统一Compose Specification 也把格式标准化了version字段现在更多是历史遗留甚至可以省略。3.2 状态对比与增量操作解析完配置之后Compose 会查询当前 Docker 环境中所有带有本项目标签的容器、网络、卷然后和配置模型里期望的状态做一次对比。这一步非常关键它是 Compose 幂等性的基础。以一个具体的例子来说明假如你改了docker-compose.yml把某个服务的镜像从 v1 换到了 v2然后执行docker compose up -d。Compose 会逐个检查现有容器发现容器的镜像和配置不一致时它会先创建新配置的容器再停止并删除旧容器——这就是常说的重建recreate。但如果你只是把某个服务的scale从 1 调到了 3Compose 会只补齐缺少的 2 个容器而不会把已运行的容器全部推倒重建。这是一个很多人没意识到的架构特点compose up 不是每次全量重建它是一个增量协调器。所以在实践中如果你只是改了环境变量或镜像版本想让容器全部重建但忘记了--force-recreate可能会惊讶地发现改了配置没生效。这不是 Compose 没检测到而是它认为当前状态已经满足要求了。反过来如果你希望无论是否已存在都按最新配置重新创建就得显式加--force-recreate参数。理解了这个机制你才会明白为什么我建议在 CI/CD 脚本里要么加--force-recreate要么用downup而不是傻傻执行一个up就以为万事大吉。3.3 依赖关系与启动顺序如果 Compose 只是创建一堆容器那跟写成多个docker run的脚本也没区别。它真正的价值在于处理容器之间的依赖关系。在docker-compose.yml里你可以在服务上声明depends_onservices: web: image: nginx depends_on: - api api: image: myapi:latest depends_on: mysql: condition: service_healthy mysql: image: mysql:8.0 healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 5s timeout: 3s retries: 10这里要特别强调一个原则depends_on控制的是创建顺序不控制可用状态。如果你只写了depends_on: - mysqlCompose 会保证 MySQL 容器先于 Web 容器启动但它不会等 MySQL 真正初始化完成。MySQL 容器启动后可能需要 20 到 30 秒才能接受连接这段时间内 Web 服务一旦启动就会连接失败然后启动失败或者不断重试。所以早期的 Compose 配置里到处都是依赖服务自己写等待脚本或者sleep 30这种东西。condition: service_healthy这个语法就聪明得多。它要求 Compose 在启动依赖方之前要等被依赖方的健康检查healthcheck先通过。这个机制背后的实现其实也不复杂Compose 在创建容器时会循环查询目标容器的状态直到它的Status变为healthy或者超过超时时间才停止。在我部署中间件比如 Nacos、数据库、配置中心的经验里健康检查是 Compose 编排可靠性的分水岭。很多人抱怨 Compose 部署的 MySQL 和 Java 微服务组合经常出现启动失败十有八九就是因为depends_on后面的条件配置没有用service_healthy而是默认的service_started。3.4 网络与卷的创建时机在容器创建之前Compose 会先检查并创建 Project 级别的网络和卷。默认情况下每个 Project 会创建一个 bridge 网络所有没有显式指定网络的 Service 都会自动加入这个网络。卷的情况类似只要配置里声明了命名的卷Compose 会在项目首次up时创建它们。这里的顺序也很重要网络一定是先于容器创建的。因为容器创建时需要指定网络配置如果网络不存在容器创建会直接报错。Compose 之所以能把这些事情组织得井井有条是因为它把网络创建、卷创建、容器创建、容器启动拆成了四个阶段依次执行而不是盲目地一个个创建。4. 网络模型Compose 架构里的通讯基础设施Compose 的网络模型是整个架构里最值得讲清楚的部分因为它直接决定容器之间能不能互相访问、和外部怎么通讯。很多人配置了ports映射就以为容器互通了其实容器之间的通讯跟端口映射完全不是一回事。4.1 服务名即主机名在一个 Compose Project 内部所有连接到默认网络的容器之间可以用服务名作为主机名hostname互相访问。比如在同一项目里你的前端服务要访问后端 API它不需要知道后端的容器 IP只需要知道 API 服务的名字services: frontend: image: nginx environment: API_HOST: http://api:8080 depends_on: - api api: image: myapi:latest在上面的配置里容器 frontend 访问http://api:8080就能到达 api 服务。这个机制不是 Compose 自己瞎搞的它依赖 Docker 内置的 DNS 服务。在同一个 bridge 网络上Docker daemon 自带一个嵌入式 DNS 服务器它会自动把服务名解析成对应容器的 IP。那为什么在默认 bridge 网络docker0上不行这是很多人踩过的一个深坑如果你用docker run --link或者直接在一个容器里 ping 另一个容器的服务名有可能失败。因为 Docker 的嵌入式 DNS 只在用户自定义网络user-defined bridge network上生效。Compose 创建的默认网络本质上是用户自定义网络所以服务名解析可靠但如果你手动把容器连到network_mode: host或者默认 bridge这个解析就失效了。4.2 端口映射与容器互通是两回事ports的作用是把容器端口暴露到宿主机它不参与容器之间的通讯。容器 A 访问容器 B走的是容器 B 在内部网络上的 IP 和服务端口容器 A 访问宿主机走的是宿主机的 IP。如果你在容器 A 里访问localhost:6379想连 Redis那是绝对连不上的——容器的localhost是容器自己。我用一个实际案例来说明假设有个项目部署了 Redis 和 API 两个服务redis配置了ports: - 6379:6379API 服务的连接配置是localhost:6379。这时 API 容器从启动起就注定连接失败因为 API 容器的 localhost 是 API 容器自己的网卡Redis 根本不在那。正确写法是redis:6379。我在代码评审里无数次看到这个问题几乎成了多容器部署的经典事故之一。所以看到一个服务连不上另一个服务时第一反应应该是检查连接配置里的主机名是否用了服务名而不是宿主机 IP 或localhost。4.3 跨项目通讯与自定义网络有时候我们需要两个 Project 之间互相访问比如一个专门跑监控一个跑业务系统。默认情况下不同 Project 的网络是隔离的但 Compose 允许你把服务加入一个已有网络只要用external关键字声明networks: shared: external: true name: my-shared-network这样这个服务就会加入到名为my-shared-network的外部网络从而和该网络上的其他容器互通。用外部网络的好处是网络生命周期由你手动管理不会因为某个项目down就被删除。这在组合多个 Compose 项目时非常实用。下面整理几个网络相关的常见问题方便排查时对照现象原因解决方向容器间无法用服务名访问网络模式是 host 或默认 bridge改为 compose 默认网络或自定义网络容器访问宿主机服务失败用了localhost访问宿主机服务使用host.docker.internal需要开启相应特性或宿主机 IP两个 Project 的服务无法通信网络隔离未加入同一网络创建共享 external 网络并加入端口映射不生效宿主机端口被占用或只映射到 IPv6检查ss -lntp占用情况、调整映射端口5. 数据卷容器状态怎么持久化Compose 架构里第二个容易出问题的部分是数据卷。容器本身是无状态的——一旦容器被删除容器内写的东西全部消失。但数据库、日志、配置文件这些必须持久化的数据就需要通过卷来挂载。5.1 三种挂载方式与使用场景Compose 支持三种数据挂载方式它们的使用场景完全不同绑定挂载bind mount把宿主机的某个目录直接挂载进容器。写法是./data:/var/lib/mysql或/path/on/host:/path/in/container。这种方式适合开发环境因为你修改宿主机的代码容器里立刻就能看到但生产环境用它会有个问题——宿主机目录的权限和路径因机器而异迁移部署时容易出岔子。命名卷named volume写法是mydata:/var/lib/mysql。卷由 Docker 管理存放位置在 Docker 数据目录内部通常/var/lib/docker/volumes/。这种方式最适合数据库这类需要持久化数据的场景因为卷的生命周期跟容器解耦容器删除之后卷还在数据不会丢。匿名卷anonymous volume没有名字写在镜像里的VOLUME指令或 Compose 配置里不带名字的卷。很少会主动去用了解即可。我的建议是生产环境里数据库、消息队列这类有状态服务一律使用命名卷只有需要频繁修改配置文件或代码时才用 bind mount。这也是为什么部署 MySQL、Nacos 时volumes配置里面几乎都是命名卷或明确的宿主机路径。5.2 Compose 对卷的命名规则跟容器命名一样Compose 对命名卷也有自己的命名规则。一般形式是project-volume比如项目名nacosdeploy、卷名data最终卷名就是nacosdeploy_data。这也解释了为什么切换项目目录名后数据会找不到——不是真的丢了而是项目名变了导致卷名变了新容器挂载的是一个新的空卷。所以再次强调有状态应用的部署项目名必须稳定最好手动指定。另一个经验是删除项目时要注意命令的区别docker compose down停止并删除项目内的容器和网络但不删除命名卷。docker compose down -v额外删除项目内使用的命名卷和匿名卷。官方文档虽然写了-v会删除卷但我见过不少同事在测试环境里头一回加-v删库成功然后在生产环境也照葫芦画瓢造成数据全清空的惨剧。我的习惯是除非明确要清空数据重建否则down后面永远不要手贱加-v。5.3 有状态中间件部署时的卷设计拿部署 Nacos 3.x 举例数据库是必须的Nacos 本身也可以挂载数据卷保存配置和日志。我一般这样设计services: nacos: image: nacos/nacos-server:v3.0.0 container_name: nacos-server ports: - 8848:8848 - 9848:9848 environment: MODE: standalone NACOS_AUTH_ENABLE: true SPRING_DATASOURCE_PLATFORM: mysql MYSQL_SERVICE_HOST: mysql MYSQL_SERVICE_PORT: 3306 MYSQL_SERVICE_DB_NAME: nacos_config MYSQL_SERVICE_USER: nacos MYSQL_SERVICE_PASSWORD: change_me volumes: - nacos_data:/home/nacos/data - nacos_logs:/home/nacos/logs depends_on: mysql: condition: service_healthy networks: - nacos_net mysql: image: mysql:8.0 container_name: nacos-mysql environment: MYSQL_ROOT_PASSWORD: root_pass MYSQL_DATABASE: nacos_config MYSQL_USER: nacos MYSQL_PASSWORD: change_me volumes: - mysql_data:/var/lib/mysql healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -uroot, -proot_pass] interval: 5s timeout: 3s retries: 20 networks: - nacos_net volumes: nacos_data: nacos_logs: mysql_data: networks: nacos_net: driver: bridge这段配置里三个卷都是命名卷MySQL 的数据在mysql_data里Nacos 的配置数据在 Nacos 的数据库中日志在nacos_logs里。任何容器删除重建都不会丢数据。网络单独建了一个nacos_netNacos 和 MySQL 只在这个内部网络中通信只有 Nacos 的 8848 和 9848 端口暴露到宿主机。我特别想提一下 9848 端口。Nacos 2.x 以后客户端还依赖 gRPC 端口也就是服务主端口 10008848 1000 9848。如果你只映射了 8848 而忘记映射 9848Nacos 的控制台可能打得开但服务注册和发现功能会在客户端报连不上。这是一个非常典型的看起来正常、实际有问题的场景部署时一定要把两个端口都映射出来。6. 版本迷局从 docker-compose 到 docker compose 的迁移这几年我在协助同事排查 Compose 问题时遇到得最多的报错就是docker: unknown command: docker compose。这个报错看起来像东西没装对实际上根因往往在于两个大版本的存在v1 和 v2。6.1 v1 与 v2 的本质差异Docker Compose 最早是 Python 写的独立工具安装之后产生一个独立的可执行文件docker-compose命令是docker-compose up -d这种用法。这是v1至今在很多老服务器上还有。到了 Docker 官方宣布集成 Compose 之后重点转向了v2用 Go 语言重写并且作为 Docker CLI 的一个插件机制分发。插件的调用方式是docker compose子命令如果你已经安装了新版 Docker一般 Docker Engine 20.10 之后都可以但没安装 compose 插件就会遇到docker: unknown command: docker compose。所以排查这个报错的第一件事是确认你的 Docker 环境是哪种组合# 查看 v2 插件是否存在 docker compose version # 查看 v1 独立工具是否存在 docker-compose version如果两个命令都没有输出说明一个都没装。如果只有docker-compose version有输出说明你还在 v1 时代要么继续用 v1 语法要么把 v2 插件补齐。如果只有docker compose version有输出说明 v2 已就绪你可以直接用新语法。6.2 Ubuntu 环境安装 Compose v2 的推荐方式在 Ubuntu 上安装 Docker Compose v2我推荐直接用 Docker 官方源安装插件包一条命令的事apt update apt install docker-compose-plugin这个包会安装 compose 插件并把它放到 Docker CLI 的插件目录中然后就可以直接使用docker compose命令了。只要你装的是 Docker Engine 20.10 以上的版本这个方式最稳妥更新也简单。另外还有一种方式是安装独立的二进制文件官方文档里提供了下载脚本curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose chmod x /usr/local/bin/docker-compose这种方式的优点是二进制版本可以手动控制方便锁定特定版本缺点是不能通过 apt 自动更新而且这样装出来的docker-compose是一个独立命令不会自动注册成docker compose插件。所以如果你装了独立二进制还想用docker compose要么加软链要么就干脆统一用docker-compose语法。我个人的建议是现代环境统一用docker compose因为 Docker 官方已经把 v2 作为标准了别再从老资料里抄docker-compose命令了。还有个常见的情况是用户在 macOS 或 Windows 的 Docker Desktop 里能用docker compose但远程连接的 Ubuntu 服务器上报 unknown command。原因就是 Docker Desktop 自带 Compose v2而服务器上的 Docker 是后来装的没有附带插件。处理方式也很简单在服务器上把插件装好两边就一致了。6.3 compose 文件格式spec与 Docker 引擎的适配除了 CLI 版本还有一个经常被忽视的东西是**compose 文件格式Compose Specification**的版本编号。你在docker-compose.yml里可能见过version: 3.8早期这是硬性要求version决定了解析器支持哪些字段。比如version: 2的文件不支持healthcheck的某些高级配置version: 3的文件对卷、网络的定义方式也不一样。到了 Compose v2解析器基本已经实现了最新的 Compose Specificationversion字段的意义减弱官方文档甚至建议去掉它但保留也不会报错。这里给一个实用的兼容原则如果你不确定服务器上的 Compose 版本尽量使用相对保守的3.8或干脆不写 version不要使用太新的字段。比如 Compose 新版本支持depends_on.long syntax的condition: service_healthy但旧版可能只支持service_started强行使用新语法会导致解析失败。还有一个所有升级过 Compose 的人都会遇到的坑v1 的docker-compose默认读取当前目录的docker-compose.yml而 v2 的docker compose也一样但两者的默认行为在一些细枝末节上有差异。比如 v2 里docker compose up默认不自动删除孤儿容器需要--remove-orphansv1 在这方面的默认行为不同。如果你在 CI 脚本里大量使用 Compose建议把常用命令的参数显式写全避免两个版本行为差异导致的不确定性。7. 部署验证与常见故障的定位思路最后这部分我结合自己长期调试 Compose 项目的经验把最常见的故障定位思路做一个梳理。这几条方法比查日志这个万能答案更具体一些。7.1 容器一直重启容器一直处于Restarting状态docker compose logs看不到明确报错。我推荐按这个顺序排查第一步看退出码。docker compose ps会显示容器的状态和退出码如果镜像里CMD启动了前台进程但进程报错退出退出码一般不是 0。第二步看启动命令和执行用户。尤其数据库、Nacos 这类有数据目录权限要求的服务经常出现目录权限不足导致进程秒退。这时候docker compose exec service ls -ld 路径或者docker compose logs里的Permission denied就能直接锁定问题。第三步看环境变量。很多镜像会根据环境变量去初始化配置比如 MySQL 的MYSQL_ROOT_PASSWORD、Nacos 的MODE。变量缺失时进程不会报错但功能不完整比如 Nacos 默认运行在集群模式本地单机部署经常看到连接集群节点失败其实就是忘了设置MODE: standalone。7.2 服务依赖不生效业务起来以后连不上数据库这个场景我在部署微服务时遇到过很多次。现象是数据库已经起来了业务容器也起来了但业务容器里连接数据库一直超时。先说根因depends_on只是保证先启动不代表可用。SQL Server、MySQL、PostgreSQL 这类数据库第一次初始化需要较长的时间你必须在业务容器里做重试连接或者用healthcheckcondition: service_healthy让 Compose 等待数据库真正可连。我的习惯是凡是中间件服务都要给它配 healthcheck凡是依赖它的服务depends_on都要写全condition: service_healthy。另外还有一个隐蔽问题业务容器里的时区、DNS 配置可能影响数据库连接。比如某些基础镜像默认时区不是本地时区可能导致数据库连接报鉴权错误这种问题排查起来很费时间。建议在镜像构建阶段就设置好时区和语言环境别指望部署时再处理。7.3 网络层面的配置变了容器之间突然不通有时候你只是改了某个服务的networks配置或者换了 bridge 网络结果容器间就访问不了了。这类问题我建议你直接进容器里验证网络docker compose exec api ping redis docker compose exec api curl redis:6379如果 ping 不通检查两个服务是否加入了同一个网络如果 ping 通但端口访问失败检查目标服务是否监听了对应的端口、防火墙有没有拦截、镜像内有没有开额外的安全组。容器网络问题定位的黄金法则是从内到外逐步验证——先确认网络连通性再确认服务监听最后确认应用层连接。7.4 升级版本后 Compose 行为不一致这里要提醒一件事从 v1 迁移到 v2 后很多命令的输出格式和默认行为有细微变化。比如docker-compose logs -f和docker compose logs -f的输出日志里容器名前缀可能不同docker-compose run --rm和docker compose run --rm的参数位置可能也不同。如果你有一套精密的 CI/CD 脚本依赖 Compose 输出做文本解析升级后一定先在测试环境跑一遍全链路不要直接推到生产。我自己就遇到过因为把docker-compose build --pull改成docker compose build --pull后输出格式变更导致下游 awk 解析直接失效的教训。8. 一点实践经验总结说回开头那个项目。后来我把那四个服务全部改成了 Compose 管理运行和维护的复杂度直线下降。现在不管是本地开发、测试环境还是单机生产我基本都遵循这样一套约定项目名显式指定用-p参数或COMPOSE_PROJECT_NAME环境变量固定下来有状态服务统一用命名卷存数据绝不随便加down -v中间件全部配置healthcheck依赖方严格使用service_healthy条件容器间的通讯地址一律用服务名不写localhost和宿主机 IP生产环境优先用docker compose命令避免 v1/v2 混用。这套约定包含了 Compose 架构里最核心的三层模型、网络设计、数据持久化和版本管理。理解这些原理不是为了背概念而是为了在出问题的时候能少走弯路。毕竟 Compose 这类工具用起来很简单用好了却需要你对它内部的逻辑有真切的感知。希望这篇内容能给你在部署和排障时提供一些实实在在的帮助。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进