☰
Docker Compose 核心原理与生产实践:从 docker run 到容器编排
2026/9/29 18:14:14 网站建设 项目流程

如果你管理过哪怕两个需要同时跑的容器,就应该能体会到:一条条 docker run 命令堆出来的环境,支撑不了几周就会乱成一团。端口记不住、启动顺序靠手速、重启一次就丢数据......这就是 Docker Compose 存在的理由:它把多个容器的启动和连接关系写进一个 YAML 文件,用docker compose up一条命令拉起整个应用栈。这篇文章围绕 Docker Compose 的核心原理与架构,从“为什么需要它”和“底层如何工作”开始,再到我用它部署 Redis、Nacos 3.x 时踩过的实际坑,最后聊一聊生产环境的使用建议,把整个链路完整过一遍。

1. 为什么需要 Docker Compose:从单容器到多服务的编排

1.1 docker run 的痛点:命令行的复杂度

很多人第一次接触 Docker 时,都是从docker run开始的。单跑一个容器确实简单,镜像、端口映射、卷挂载、环境变量,一条命令就能搞定。但一旦应用变成多服务架构,事情立刻就不对劲了。

举一个很常见的例子:一个 Web 应用,前面有 Nginx,后面有 PHP 或 Java 服务,再搭配 MySQL 和 Redis。你至少要敲四条docker run,每条都是一长串参数:

docker run -d --name mysql -v mysql-data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root mysql:8 docker run -d --name redis redis:7 docker run -d --name app --link mysql --link redis my-app:latest docker run -d --name nginx -p 80:80 --link app nginx:latest

这几个参数看着不多,实际维护起来全是坑。容器的 IP 每次创建都会变,--link已经是过时方案;想改端口、换镜像版本、加环境变量,就得先docker stop再重新拼命令;机器重启后,手动拉起全部服务还要记得顺序。最难受的是,这些信息全在终端历史记录里,换个环境、换个同事,一切都得从头来。

1.2 Compose 的定位:声明式地描述整个应用

Docker Compose 解决的不只是“省几行命令”的问题,它把“如何启动一组容器”这件事从命令式变成了声明式。你在 YAML 文件里描述目标状态:要跑哪些服务、用什么镜像、开哪些端口、挂哪些卷、服务之间怎么依赖。然后docker compose up负责把当前环境变成这个目标状态。

打个比方,docker run像你打电话告诉工人“这里砌堵墙,那里开扇窗”,而 Compose 像给了一张装修图纸。图纸上不会规定工人每一步怎么走,但明确了最终的房子长什么样。所以 Compose 的配置文件本身就是项目的一部分,可以被评审、被版本管理,别人拿到仓库就能复现出一模一样的运行环境。

这里还要说清楚 Compose 的定位边界:它擅长的是单机上的多容器应用编排,不是跨多台机器的集群管理。单机情况下,Compose 是绝对主力;涉及多机、自动伸缩、故障转移,那要看 Kubernetes 或 Swarm,但 Swarm 现在基本已经不活跃了,Kubernetes 的复杂度跟 Compose 完全不在一个量级。所以在本地开发、测试环境、甚至是小规模生产部署里,Compose 是最划算的选择。

顺便提醒一句命令的差异:旧版叫docker-compose,中间有横线,是 Python 写的;新版叫docker compose,是 Docker 官方内置的 Go 插件。很多人报docker: unknown command: docker compose,多半就是 Docker 版本太老或者没有安装 compose 插件,这个问题我在第 5 章专门讲。

2. Compose 核心原理:Service、Network 与 Volume 如何协作

2.1 三大抽象:Service、Network、Volume

Compose 的 YAML 里看起来字段很多,剥掉外壳后核心只有三个抽象:服务(Service)、网络(Network)、卷(Volume)。

Service 是最直观的一层,它描述“我要跑什么容器”。镜像、端口映射、环境变量、启动命令、依赖关系、健康检查,都是 Service 的范畴。你可以把 Service 理解为“带了一堆运行参数的容器”,但 Compose 不会直接创建一个名为 app 的容器,而是把它纳入整个项目进行管理,所以资源命名都会带项目名前缀。

Network 解决的是“容器之间怎么通信”。Compose 会自动创建一个默认网络,同一个 compose 项目里的所有服务都会加入这个网络,容器之间通过服务名就能直接访问。这个机制太关键了:你的应用代码里连接数据库,不需要写localhost,也不需要写容器的随机 IP,直接写服务名db就行。自定义网络还可以设置 driver、子网、别名,甚至通过external接入一个已经存在的网络。

Volume 解决的是“数据往哪里放”。容器本身是无状态的,删掉重建就什么都没了。Compose 里可以用命名卷、绑定挂载、匿名卷三种方式把数据持久化或共享出去。命名卷由 Docker 管理,位置不固定但备份方便;绑定挂载直接映射宿主机目录,适合配置文件、开发代码、日志输出;匿名卷一般是不推荐在生产用的,除非你有特别理由。

2.2 从 YAML 到运行容器:一条 up 命令背后发生了什么

docker compose up看似一条命令,背后其实走了一整套流程。理解这套流程,排查问题时才不会瞎猜。

先把流程拆开看:

  1. 读取docker-compose.yml(以及 override、环境变量文件),解析出项目定义。
  2. 确定 project name,默认取当前目录名,也可以通过-p参数或COMPOSE_PROJECT_NAME环境变量指定。project name 会作为容器名、网络名、卷名的前缀,多个项目就不会互相干扰。
  3. 创建项目专属的网络和卷。
  4. 检查镜像是否存在,不存在就拉取;如果配置了build,还会先构建镜像。
  5. 按依赖关系创建和启动容器。
  6. 容器启动后,如果配置了 healthcheck,Compose 会在依赖服务健康后才继续拉起后面的服务。

为什么 project name 这么重要?我见过有人把两个项目放在同一个目录名下面,结果容器名冲突,后启动的把先启动的挤掉。-p参数就是这时候救命的:你可以在 CI 流水线里给每个分支一个独立项目名,互不干扰。

up还有一个特性值得注意:它是幂等的。配置没变时,重复执行docker compose up -d不会重建容器;只有镜像、环境变量、端口这些关键配置变化时,Compose 才会重建对应的服务。这个行为跟 Kubernetes 里的 reconcile 逻辑很像,都是持续让实际状态收敛到目标状态。

2.3 depends_on 不是万能的:启动顺序与就绪判断

新手最容易被depends_on骗了。以为写了depends_on: - db,数据库就一定能连上,结果应用还是报错connection refused。

原因是:depends_on只保证“先启动 db 容器”,但容器进程启动成功不代表服务已经就绪。MySQL 容器起来了,内部可能还在初始化数据目录、监听端口还没打开;应用容器这时候去连,自然失败。

真正的做法是组合healthcheck和condition: service_healthy:

services: db: image: postgres:16 healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"] interval: 5s timeout: 3s retries: 10 app: image: my-app:latest depends_on: db: condition: service_healthy

这里有个容易踩的小坑:Compose 会用宿主环境先展开$符号。如果你在 YAML 里写$POSTGRES_USER,它可能被宿主机拿过去展开成空值。写$${POSTGRES_USER},Compose 才会把它当成容器内部的环境变量透传下去。我第一次写就直接踩了,容器日志里全是role "root" does not exist,排查了半天。

3. 架构视角:一个可维护的 Compose 项目怎么组织

3.1 目录布局与服务拆分

很多人的 Compose 项目长这样:所有配置堆在一个大目录里,docker-compose.yml三百行,.env里塞了二十多个变量,每个服务的配置散落在不同地方。这样不是不能用,但维护两三个月后,基本就没人敢动了。

我习惯的结构是每个服务一个子目录,各自带 Dockerfile 和配置;公共的编排文件放在项目根目录:

my-app/ ├── docker-compose.yml ├── .env ├── .dockerignore ├── backend/ │ ├── Dockerfile │ └── app.py ├── frontend/ │ ├── Dockerfile │ └── nginx.conf ├── redis/ │ └── redis.conf └── db/ └── init.sql

这样划分有一个很实际的理由:镜像构建上下文(build context)必须尽量干净。如果你把整个仓库作为 Dockerfile 的构建上下文,构建时会把无关代码、.git、node_modules 全部打包发给 daemon,既慢又容易踩到缓存失效的问题。每个服务单独一个上下文,再配合.dockerignore,构建速度立竿见影。

Compose 文件本身也应该拆分职责。base 文件放通用配置,override 文件放环境差异,敏感信息交给.env和环境变量。这个我在 6.3 节还会展开。

3.2 .env、env_file 与变量替换

Compose 的变量机制有三个容易混淆的概念:.env、env_file、environment。

.env文件是给 Compose 自身做变量替换用的。你在 YAML 里写的${DB_PASSWORD},Compose 会去当前目录的.env里找DB_PASSWORD的值,然后渲染进配置。这个文件不应该提交到 git,里面通常放密码、token 这类敏感信息。

env_file是把一组KEY=VALUE导入到容器内部,作为容器进程看到的环境变量。它影响的是容器里跑的程序,不参与 Compose 配置渲染。

environment则是直接在 YAML 里写环境变量,优先级最高,会覆盖env_file的同名变量。

实际使用中最有用的语法是默认值:

services: api: image: my-api:latest environment: DB_HOST: ${DB_HOST:-postgres} LOG_LEVEL: ${LOG_LEVEL:-info}

${DB_HOST:-postgres}的意思是:如果.env或宿主环境里定义了DB_HOST就用它,否则用默认值 postgres。这个写法能让你在不同环境之间切换时不需要改 YAML,只需要改.env。

一个经常被忽略的调试技巧是docker compose config。它会把你所有的变量替换、override 合并、默认值填充都渲染成最终的配置输出。改完 YAML 怀疑某个变量没生效,先跑一下docker compose config,基本能看出来问题在哪。

3.3 自定义网络与服务发现

Compose 默认会给每个项目建一个网络,所有服务挂在里面。默认行为下,服务之间用服务名互相访问已经够用了,但有几种场景你需要显式定义自定义网络。

第一种是隔离场景。有的服务不希望被同项目里其他服务访问,可以拆到不同网络。比如数据库只允许后端服务访问,不要暴露给 Nginx。

第二种是固定 IP 或特定网段的场景。有些老应用代码里硬编码了 IP 而不是服务名,这种时候你可以在自定义网络里用ipam指定网段,然后给服务配ipv4_address。

第三种是接入外部网络的场景。两个独立的 Compose 项目需要互相通信,它们各自建的默认网络是隔离的。解决方案是提前创建一个外部网络,两边都用external引用它:

docker network create shared-net
networks: shared-net: external: true services: app: image: my-app:latest networks: - shared-net

还有一点必须提醒:ports的"6379:6379"是把容器的 6379 端口暴露到宿主机上。如果服务只在 Compose 内部被其他容器访问,完全不需要暴露端口。暴露端口等于把服务放到了宿主机网络上,多一个入口就多一分被扫描的风险。内部通信时,直接使用服务名和内部端口即可。

4. 典型场景实操:Redis 与 Nacos 3.x 部署实录

4.1 用 Compose 快速部署 Redis 并开启持久化

Redis 是我用 Compose 部署得最多的中间件,没有之一。本地开发、测试环境、生产环境的单节点缓存,一套配置基本通用。

先看一个完整可用的示例:

services: redis: image: redis:7-alpine container_name: local-redis ports: - "6379:6379" volumes: - redis-data:/data command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"] healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 restart: unless-stopped volumes: redis-data:

对应的.env文件:

REDIS_PASSWORD=ChangeMe_2025

操作步骤很简单:docker compose up -d,然后docker compose ps看状态,再用docker exec -it local-redis redis-cli -a ${REDIS_PASSWORD} ping验证。

这里有几个细节值得展开。--appendonly yes开启 AOF 持久化,数据会写入/data,而这个路径挂载到了命名卷redis-data上。容器删了、网络换了、镜像升级了,数据都还在。但要小心docker compose down -v,-v会连卷一起删,数据就真的没了,我见过不止一个人因为习惯性加-v把本地 Redis 数据清空。

restart: unless-stopped表示除非手动 stop,否则 Docker daemon 重启时容器会自动拉起。这个在开发和轻量生产环境里非常有用,至少机器重启后服务能自己回来。

再说密码的问题。开发环境很多人图省事不设密码,但如果 Redis 端口映射到了宿主机,又在云主机上没配安全组,几分钟就会被扫描器盯上。用${REDIS_PASSWORD}而不是硬编码,最大的好处是配置文件和代码分离,密码只存在于.env,就算 YAML 被人看到了也不会泄露密钥。

4.2 用 Compose 部署 Nacos 3.x:踩过的三个坑

Nacos 是常用的注册中心和配置中心。3.x 和 2.x 在 Compose 部署上的参数思路基本一致,最大的区别不在 YAML,而在你有没有真正理解它的网络和数据依赖。这里以 3.2.x 版本为例讲三个我实际踩过的坑。

第一个坑是端口。很多人只放行了 8848 控制台端口,然后客户端怎么都连不上。如果客户端使用的 gRPC 端口是主端口加 1000,也就是 8848 + 1000 = 9848。这个端口需要在宿主机上暴露,并且防火墙、安全组也必须放行,否则注册服务超时是必然的。

第二个坑是数据库就绪。Nacos 默认情况下可以用内嵌数据库,但生产环境一般要切到 MySQL。问题是 MySQL 容器启动后,Nacos 容器可能已经启动并连库失败。常规的depends_on管不了这个,必须用健康检查。

第三个坑是初始化 SQL。Nacos 的配置表需要提前建好,否则 Nacos 虽然能起来,控制台里全是“表不存在”的错。用 MySQL 官方镜像的时候,把官方 SQL 放到./init目录里挂载到/docker-entrypoint-initdb.d,首次启动会自动执行,省去手动导入的麻烦。

下面是一个可落地的组合:

services: mysql: image: mysql:8.0 container_name: nacos-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: nacos_config volumes: - mysql-data:/var/lib/mysql - ./init:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-proot123"] interval: 5s timeout: 3s retries: 10 nacos: image: nacos/nacos-server:v3.2.0.1 container_name: nacos-server depends_on: mysql: condition: service_healthy ports: - "8848:8848" - "9848:9848" environment: MODE: standalone SPRING_DATASOURCE_PLATFORM: mysql MYSQL_SERVICE_HOST: mysql MYSQL_SERVICE_DB_NAME: nacos_config MYSQL_SERVICE_PORT: 3306 MYSQL_SERVICE_USER: root MYSQL_SERVICE_PASSWORD: root123 NACOS_AUTH_ENABLE: "true" NACOS_AUTH_TOKEN: "这里填一个至少32位的随机字符串" NACOS_AUTH_IDENTITY_KEY: "serverIdentity" NACOS_AUTH_IDENTITY_VALUE: "security" volumes: mysql-data:

注意这里的MYSQL_SERVICE_HOST: mysql,不是 IP 也不是 localhost,而是 Compose 网络里的服务名。Nacos 容器在启动时会通过服务名去解析 MySQL 的地址,这个能力是 Compose 默认网络给的。如果你尝试把 Nacos 跑在宿主机上、把 MySQL 跑在容器里,才会踩到localhost不通的坑。

部署完成后访问http://宿主机IP:8848/nacos,默认账号密码在开启了鉴权之后需要你按文档重新配置。强烈建议不要裸奔:注册中心暴露在公网上、没有鉴权,基本等于把服务发现信息直接送人。

4.3 完整的组合编排:让 Redis 和 Nacos 一起跑

如果你既需要 Redis 又需要 Nacos,把它们放进同一个 compose 项目里反而要注意服务名冲突。我的习惯是:中间件项目独立成一个目录,和业务应用分开编排。因为中间件的生命周期和业务代码完全不同,业务发版频繁,中间件几乎不动,硬塞在一起会让docker compose up每次都要检查一遍不相关的服务。

组合编排时,重点是网络规划。Redis 和 Nacos 都要通过服务名互相访问,但它们未必需要暴露到宿主机。如果只有容器内的应用需要连接,Redis 的ports可以完全去掉;Nacos 的客户端如果想从宿主机访问,8848 和 9848 才需要保留。

所以在真实项目里,我更倾向于拆成两个 compose 文件:一个管中间件(Redis、MySQL、Nacos),一个管业务应用,中间用一个 external 网络连接。这样既保持了分组清晰,又不会让单个文件的职责过载。具体 external 网络的用法在前面 3.3 已经说过了,这里不再重复。

5. 高频报错与排查实录:终端里最常见的几个坑

5.1 docker: unknown command: docker compose:不是命令的问题,是插件没装

这个报错的热度一直很高。很多人输入docker compose却提示 unknown command,第一反应是命令敲错了,其实核心原因通常是 Docker 版本太老,或者没有安装 Compose V2 插件。

判断方法很简单:

docker version

如果客户端版本低于 20.10,建议直接升级 Docker 本身,因为新版 Docker 已经内置了 compose 插件。Ubuntu 上的标准安装方式是把官方仓库的几个包一起装:

sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker compose version

看到Docker Compose version v2.x.x就说明插件装好了。

这里有个容易迷惑的点:旧命令docker-compose(带横线)是 Python 写的 V1,已经停止维护很多年。新写的项目如果还在用 V1,遇到语法不支持、性能问题,最好是直接迁移到docker compose。迁移成本很低,绝大多数 V1 的 YAML 在 V2 下都能直接跑,只有少量version声明和个别字段需要调整。

如果你更习惯给当前 Linux 用户加 docker 权限而不是每次 sudo,执行:

sudo usermod -aG docker $USER newgrp docker

然后重新登录终端。不加这一步,后面所有 compose 命令都要带 sudo,而且 sudo 环境下拉取的环境变量、配置文件会不一致,容易产生“明明安装成功但命令找不到”的错觉。

5.2 unexpected architecture type in ...:架构不匹配导致的一连串问题

这个报错看起来像程序直接抛异常,实际排查下来大部分是架构不匹配。有人下载了 Docker 二进制包,解压后一执行就报unexpected architecture type;有人拉取镜像后容器一启动就崩溃,日志里同样出现类似的关键字。

先说二进制包的问题。Docker Compose 的二进制安装包针对 amd64、arm64 等不同架构分别发布,如果你在 ARM 机器上下了 amd64 的包,或反过来,运行时就可能报 architecture 相关错误。检查宿主机架构用一条命令:

uname -m

x86_64 对应 amd64,aarch64 对应 arm64。下载时一定要对照着选,别只看版本号。

再说镜像的问题。Compose 默认会按宿主机架构拉取对应平台的镜像,但有两个例外。一种是你手工拉取了非本机平台的镜像,比如在 ARM 开发板上用 x86 的镜像,容器能启动但很可能会崩溃;另一种是镜像的 manifest 不包含当前平台,Docker 会报no matching manifest for linux/arm64之类的话。解决办法是显式指定平台:

docker pull --platform linux/amd64 redis:7-alpine

或者在 Compose 文件里写死:

services: redis: image: redis:7-alpine platform: linux/amd64

但是要明白,强制换平台只是“能跑”,性能上会打折扣,因为要通过模拟层执行。生产环境还是要优先使用支持 multi-arch 的官方镜像,或者用docker buildx build --platform linux/amd64,linux/arm64自己构建多架构镜像。

5.3 端口冲突、容器名占用与其他常见的 Compose 疑难点

Compose 的报错信息大多数都很直白,但有些错是因为对生命周期理解不到位。我整理了一个速查表,都是实际排查中最高频的几个:

报错或现象常见原因处理方式
Bind for 0.0.0.0:6379 failed: port is already allocated端口被宿主机其他进程或其他容器占用用 `ss -lntp
The container name "/xxx" is already in use之前创建的同名容器没有被清理清理旧容器,或检查是否两个 Compose 项目重名
Service "db" depends on undefined service "mysql:xxx"depends_on写的服务名和实际不一致检查 services 下服务名拼写,depends_on必须使用服务名而不是容器名
network xxx declared as external, but could not be found引用了外部网络但没有提前创建docker network create xxx,或去掉external: true
.env里的变量没生效.env必须和 compose 文件在同一目录,且变量名要能被替换到用docker compose config查看渲染结果
docker-compose: command not found安装的是 V2 插件,旧命令不可用改用docker compose,或单独安装 V1(不推荐)

还有一个非常容易忽略的坑:Compose 默认网络中的容器,通过容器名也可以访问,但通过服务名访问才是正确姿势。容器名可能在--force-recreate后变化,服务名是稳定的。所以应用配置里的数据库地址、Redis 地址,一律写服务名。

6. 生产环境使用 Compose 的进阶经验

6.1 版本锁定与配置可复现

生产环境最忌讳的就是“某天突然跑不起来,因为镜像悄悄变了”。Compose 里的 image 字段如果写redis:latest,等于把命运交给了镜像仓库。正确的做法是锁定精确版本,比如redis:7.4.2-alpine,Dockerfile 里也尽量用带具体版本的基础镜像。

可复现还有一个隐藏因素:构建上下文。同一份 Dockerfile,在不同时间构建出来的镜像可能因为依赖源版本浮动而不同。要彻底锁定,需要同时固定基础镜像的 digest(sha256 摘要):

services: api: image: my-api:1.2.3

如果镜像已经发布到了私有仓库,用 digest 锁定更稳:

services: api: image: my-registry.example.com/my-api@sha256:xxxxxxxx

配置变更后,建议把docker compose config的输出保存为deploy.yml,当作某个发布版本的“配置快照”。以后回滚时,除了切镜像版本,配置也能对齐。

6.2 资源限制与日志轮转

容器默认是可以吃满宿主机的 CPU 和内存的。如果你的生产机上同时跑着业务和数据库,一个容器异常占用内存,可能导致所有服务一起翻车。Compose 支持为服务设置资源限制:

services: app: image: my-app:1.0.0 deploy: resources: limits: cpus: "0.50" memory: 256M

这段配置的含义是:该容器最多使用 0.5 个 CPU 核心和 256MB 内存。超出限制时,容器会被限制或触发 OOM,而不是拖垮整个节点。这个字段在 Compose V2 里是支持的,虽然名字带deploy,但在单机场景下也生效。

日志轮转是很多人到磁盘报警才想起来的问题。Docker 默认 json-file 日志驱动没有大小上限,一个话痨应用几天就能把磁盘塞满。我习惯在每个 compose 服务里加上:

services: app: image: my-app:1.0.0 logging: driver: json-file options: max-size: "10m" max-file: "3"

这样每个容器的日志文件最多 10MB,保留 3 个历史文件,30MB 封顶。在生产环境,这是成本最低的“防磁盘爆掉”手段。

6.3 override 与 profiles:一套文件管多个环境

同一套代码库,开发环境和生产环境往往只有细微差别:端口不同、日志级别不同、是否开启调试服务不同。如果每改一个环境就复制一份 compose 文件,后面维护会非常痛苦。Compose 的 override 机制天生就是为这个场景设计的。

默认情况下,如果目录下同时存在docker-compose.yml和docker-compose.override.yml,执行docker compose up时后者会自动叠加在前者上。所以你可以把通用配置写在基础文件里,开发环境特有的端口、挂载、调试参数写在 override 文件里,生产环境则用显式指定:

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

-f可以叠加多个文件,后面的文件会覆盖前面的同名配置。这样一套代码,三种环境,不需要复制粘贴大量的 YAML。

profiles机制则适合“同一环境下按需拉起”的服务。比如开发环境需要 MailHog 接收测试邮件,生产可不需要,那就给它打上 profile:

services: mailhog: image: mailhog/mailhog profiles: ["dev"]

默认docker compose up -d不会启动 mailhog,只有带上 profile 才会:

docker compose --profile dev up -d

这个机制能让一个 compose 项目同时容纳常用服务和可选辅助服务,文件结构反而更清晰。

6.4 healthcheck 与优雅停止,比想象中更重要

healthcheck 在整个 Compose 架构里承担的角色远比两行配置看起来重要。它不仅是depends_on的条件判断来源,也是服务状态观测的最直接手段。启动服务后执行docker compose ps,如果配置了健康检查,状态列会显示healthy还是unhealthy,一眼就能看出问题。

健康检查本身也要设计得合理。interval不要太短,否则容器刚启动还在初始化,检查就直接失败并计入 retries;retries不要太少,数据库冷启动有时候需要十几秒,你设置 3 次重试、每次间隔 2 秒,还没就绪就标成 unhealthy 了。我一般把 interval 设在 5~10 秒,retries 设在 10 次以上,给慢服务足够的启动时间。

优雅停止是另一个容易被忽略的环节。容器收到停止信号后,应用需要时间处理完正在进行的请求、关闭连接池、刷盘。默认的stop_grace_period是 10 秒,对于很多应用来说并不够。可以在 Compose 里调大:

services: app: image: my-app:1.0.0 stop_grace_period: 60s

同时,尽量给容器配置init: true。不加 init 时,容器内的 PID 1 就是你的主程序进程,它如果不擅长处理子进程和信号转发,停容器时就会出现僵尸进程、请求没来得及完成就被杀。init: true会让 Docker 在容器里启动一个轻量 init 进程来管理系统信号,这是一个性价比极高的配置。

我再分享一个实际运维里的心得:Compose 虽然定位是单机编排,但它承担了我在很多项目里“基础设施代码化”的第一棒。Redis、Nacos、Postgres、Prometheus,全都可以用 Compose 管理起来。只要命名规范、变量清晰、healthcheck 到位,它的稳定程度完全撑得起小规模生产环境。每次觉得配置混乱的时候,docker compose config能帮你把层层叠叠的变量一次性摊开看明白。

最后留一个小技巧:给每个服务都写 healthcheck,哪怕只是redis-cli ping。因为 Compose 只有在配置了健康检查之后,depends_on的service_healthy条件才有意义。很多“服务之间连不上”的问题,本质不是网络不通,而是启动顺序和就绪判断没做好。这十个字,值得贴到终端上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询