Skip to content

Docker Compose 从项目模型、生命周期到商业编排

Docker Compose 不只是“把多条 docker run 写进一个 YAML”。它会把一个应用栈解析成项目模型,再调用 Docker Engine 创建容器、网络、Volume、Secret 和其他对象。真正需要掌握的是:项目名如何决定资源名、配置如何插值和合并、up 为什么会重建容器、depends_on 为什么不能代替应用重试、服务名为什么能做 DNS、down -v 为什么可能删除数据,以及 Compose 到底适不适合生产。

学习目标

学完本页,应能:

  1. 解释 Compose CLI、Compose Model、Docker Engine 和容器运行时之间的调用关系。
  2. 区分 service、container、project、network、volume、config、secret。
  3. 解释项目名、服务名、容器名、DNS 名和资源名前缀的关系。
  4. 看懂 upstartstoprestartdownrunexec 的行为差异。
  5. 分清 YAML 变量插值、env_fileenvironment、镜像 ENV 和最终容器环境。
  6. 正确使用健康检查和 depends_on.condition,同时在应用层实现超时、重试和熔断。
  7. 设计 Compose 网络、端口、Volume、Secret、Profile 和多文件覆盖。
  8. 写出 Spring Boot、MySQL、Redis 的可运行商业场景 Demo。
  9. 从配置错误、依赖未就绪、DNS、端口、权限、重建和数据丢失等现象定位根因。
  10. 说明 Compose 与 Docker Swarm、Kubernetes 的边界。

一、Compose解决的是“一个应用栈如何被声明和重复创建”

没有 Compose 时,开发者可能手工执行:

bash
docker network create order-net
docker volume create order-mysql-data
docker run -d --name mysql --network order-net ... mysql:8.0
docker run -d --name redis --network order-net ... redis:7
docker run -d --name order-api --network order-net -p 8080:8080 ... order-api:1.0

这组命令的问题不是“太长”这么简单:

  • 参数散落在 Shell 历史、Wiki 和个人电脑中。
  • 很难判断某个容器当前配置是否和团队约定一致。
  • 容器创建顺序、网络、Volume、健康检查容易遗漏。
  • 改一次配置后,不知道应该重启还是重建哪些容器。
  • 不同开发者可能创建同名资源并相互干扰。

Compose 将期望状态写进 compose.yaml

text
项目需要哪些服务
每个服务使用什么镜像
如何构建
加入哪些网络
挂载哪些数据
暴露哪些端口
使用什么环境变量
有什么启动依赖
如何判断健康
如何停止和重启

它让配置可以进入 Git、代码评审和自动化流程,但不会自动使架构具备高可用、备份、滚动发布或跨主机调度能力。

二、Compose V1、V2与Compose Specification

历史上常见命令是:

bash
docker-compose up -d

这是带连字符的独立 Compose V1 工具。现代 Docker 通常使用 Compose V2 插件:

bash
docker compose up -d

推荐以当前 Compose Specification 和实际安装的 V2 版本为准:

bash
docker compose version
docker version

旧文章中的顶层:

yaml
version: "3.8"

在现代 Compose Specification 中通常不再用于选择一套互斥语法版本,甚至可能收到“obsolete”提示。团队应通过工具版本、CI 校验和 docker compose config 确认功能支持,不能只凭 version: "3.8" 判断行为。

三、Compose工作原理:YAML不会直接变成进程

mermaid
flowchart TD
    A["compose.yaml与覆盖文件"] --> B["Compose CLI读取配置"]
    B --> C["变量插值与文件合并"]
    C --> D["校验并生成Compose项目模型"]
    D --> E["比较期望模型和现有资源"]
    E --> F["调用Docker Engine API"]
    F --> G["创建或复用Network与Volume"]
    F --> H["拉取镜像或执行build"]
    F --> I["创建或重建Container"]
    I --> J["containerd与OCI runtime启动进程"]

Compose 本身不是新的容器运行时。它是面向“多服务项目”的客户端编排层:

  1. 读取配置。
  2. 解析成统一项目模型。
  3. 调用 Docker Engine API。
  4. Docker daemon 再负责镜像、容器、网络和 Volume。
  5. 最终仍由 containerd、OCI runtime 和 Linux 内核运行容器进程。

因此,docker compose ps 中的容器也能被普通命令查看:

bash
docker ps
docker inspect <容器>
docker network inspect <项目网络>
docker volume inspect <项目卷>

四、先分清Project、Service和Container

4.1 Project是资源隔离边界

假设目录名为 order-platform,配置为:

yaml
services:
  api:
    image: order-api:1.0
  mysql:
    image: mysql:8.0

Compose 通常会创建类似:

text
order-platform-api-1
order-platform-mysql-1
order-platform_default

其中:

  • order-platform 是项目名。
  • apimysql 是服务名。
  • order-platform-api-1 是某个服务实例对应的容器名。
  • order-platform_default 是项目默认网络。

4.2 项目名从哪里来

常见控制方式:

bash
docker compose -p order-dev up -d

或者:

bash
COMPOSE_PROJECT_NAME=order-dev docker compose up -d

也可以在配置中声明:

yaml
name: order-dev

若都没有,通常根据 Compose 项目目录名推导。实际优先级和特殊多文件场景应使用:

bash
docker compose config
docker compose ls

确认,不要凭容器名前缀猜测。

4.3 为什么项目名改变后“数据库数据不见了”

配置:

yaml
services:
  mysql:
    image: mysql:8.0
    volumes:
      - mysql-data:/var/lib/mysql

volumes:
  mysql-data:

项目名是 order-dev 时,实际命名卷可能是:

text
order-dev_mysql-data

项目名改成 order-test 后,Compose 创建:

text
order-test_mysql-data

MySQL 挂载的是新的空卷,看起来像“原数据丢了”,但旧卷可能仍存在。排查:

bash
docker compose ls
docker volume ls
docker compose config
docker inspect <mysql容>

4.4 为什么不建议滥用container_name

yaml
services:
  api:
    container_name: order-api

硬编码容器名会:

  • 让不同 Compose 项目更容易发生名称冲突。
  • 破坏按项目名自动隔离的效果。
  • 通常阻碍同一服务扩展多个副本,因为容器名必须唯一。
  • 让调用方错误依赖某个实例名,而不是服务发现名称。

服务间通信应优先使用服务名 apimysql,而不是手工固定容器名或 IP。

五、Compose文件结构

最常见顶层对象:

yaml
name: order-platform

services:
  api: {}
  mysql: {}

networks:
  backend: {}

volumes:
  mysql-data: {}

secrets:
  mysql-password: {}

configs:
  nginx-conf: {}
顶层对象解决的问题
services定义可创建一个或多个容器实例的服务模板
networks定义容器网络连接关系
volumes定义持久数据对象
secrets定义需要以文件等方式授予服务的敏感输入
configs定义非敏感配置文件
name明确项目名

Service 不是正在运行的容器。Service 是容器配置模板;容器是这个模板创建出来的实例。

六、先用docker compose config看“最终配置”

原始 YAML 不一定等于最终生效配置,因为中间还可能发生:

  • 环境变量插值。
  • .env--env-file 读取。
  • 多个 -f 文件合并。
  • profiles 过滤。
  • YAML 锚点展开。
  • 相对路径解析。
  • Compose 默认值补全。

因此修改后先执行:

bash
docker compose config
docker compose config --services
docker compose config --volumes
docker compose config --profiles

仅校验并安静退出:

bash
docker compose config --quiet

注意:展开后的配置可能包含环境变量中的密码或 Token,复制到工单和群聊前必须脱敏。

七、变量插值与容器环境变量是两条链路

这是 Compose 最常见的误区。

7.1 第一条链路:Compose解析YAML时插值

配置:

yaml
services:
  api:
    image: "order-api:${APP_VERSION:-latest}"
    ports:
      - "${HOST_PORT:-8080}:8080"

Compose 在创建容器之前先替换 ${APP_VERSION}${HOST_PORT}。这些值可能来自当前 Shell、.env 或显式 --env-file

查看插值环境:

bash
docker compose config --environment
docker compose config

常用语法:

yaml
image: "order-api:${APP_VERSION:-latest}"       # 未设置或为空时用latest
image: "order-api:${APP_VERSION-default}"       # 未设置时用default
image: "order-api:${APP_VERSION:?must be set}"  # 缺失时直接报错

需要把 $ 原样传进容器命令而不让 Compose 提前插值时,常用双写:

yaml
healthcheck:
  test: ["CMD-SHELL", "echo $$HOSTNAME"]

${变量} 是 Compose 解析阶段;$$变量 通常用于延迟到容器内 Shell 再展开。

7.2 第二条链路:给容器设置环境变量

yaml
services:
  api:
    env_file:
      - ./app.env
    environment:
      SPRING_PROFILES_ACTIVE: dev
      LOG_LEVEL: INFO

这些值进入容器 .Config.Env,应用进程才可能读取:

bash
docker compose up -d
docker compose exec api env
docker inspect --format '{{json .Config.Env}}' <api容>

7.3 .env不等于env_file

文件主要作用
项目 .env为 Compose 文件本身提供插值值,也可能影响 Compose CLI 行为
service 的 env_file将变量放入指定服务容器的环境

一个值出现在 .env 中,不代表它一定进入容器;必须在 Compose 最终模型中被引用,或者通过 env_fileenvironment 等注入。

7.4 最终值冲突怎么判断

不要靠记忆猜复杂优先级。分两步验证:

bash
# 第一步:Compose解析和合并后的模型
docker compose config

# 第二步:容器实际获得的环境
docker compose up -d
docker inspect --format '{{json .Config.Env}}' <>

一般应明确这些来源:

  • 镜像 Dockerfile 的 ENV 默认值。
  • service env_file
  • service environment
  • Compose 插值所使用的 Shell、.env--env-file
  • docker compose run -e 等命令行覆盖。

敏感值不应因为“优先级清楚”就放入普通环境变量;环境变量可能被 inspect、进程信息、诊断包和日志看到。

八、up并不只是“start所有容器”

执行:

bash
docker compose up -d

Compose 大致会:

mermaid
flowchart TD
    A["解析最终项目模型"] --> B["创建缺失的网络和Volume"]
    B --> C["拉取镜像或执行build"]
    C --> D{"服务容器是否存在"}
    D -- "不存在" --> E["创建容器"]
    D -- "存在" --> F{"配置或镜像是否需要更新"}
    F -- "需要" --> G["停止并重建容器"]
    F -- "不需要" --> H["复用现有容器"]
    E --> I["按依赖关系启动"]
    G --> I
    H --> I
    I --> J["前台聚合日志或后台返回"]

常用选项:

bash
docker compose up -d
docker compose up --build -d
docker compose up --pull always -d
docker compose up --force-recreate -d
docker compose up --no-recreate -d
docker compose up --remove-orphans -d
docker compose up --wait -d
选项含义与风险
--build启动前构建有 build 配置的服务,不表示一定忽略缓存
--pull always尝试拉取镜像,适合明确需要检查远端更新的流程
--force-recreate即使配置看起来没变也重建容器,会造成服务中断
--no-recreate已存在容器不重建,可能导致新配置没有生效
--remove-orphans删除不在当前模型中的同项目容器,执行前确认项目名和覆盖文件
--wait等待服务达到 running/healthy,要求健康定义合理且版本支持

8.1 重建容器不等于删除命名卷

重建会删除旧容器及其可写层,再基于镜像创建新容器。显式命名卷通常重新挂载,因此数据可以保留;但容器可写层中的数据会消失。

这也是为什么应用不能把数据库、上传文件或唯一业务数据只写在容器可写层。

8.2 镜像标签不变也可能产生认知混乱

如果一直使用:

yaml
image: order-api:latest

本地已有 latest 与远端 latest 可能不是同一内容。是否拉取、何时重建、当前容器使用哪个 Image ID 都要检查:

bash
docker compose images
docker compose pull
docker inspect --format '{{.Image}}' <>
docker image inspect order-api:latest

生产更适合使用版本、commit Tag,并在部署记录中保存 Digest。

九、生命周期命令必须分清

命令核心行为容器默认网络命名卷
create创建但不启动创建创建创建或复用
start启动已有容器保留保留保留
stop发停止信号并等待保留保留保留
restart停止后再启动已有容器保留保留保留
kill发送指定信号,默认强杀保留保留保留
rm删除已停止服务容器删除通常保留默认不删命名卷
down删除项目容器和默认网络删除删除默认保留命名卷
down -v在 down 基础上删除声明卷删除删除可能删除,危险

停止:

bash
docker compose stop -t 30

再次启动同一批容器:

bash
docker compose start

删除项目容器和网络但保留普通命名卷:

bash
docker compose down

删除项目并同时处理 Volume:

bash
docker compose down -v

对数据库环境执行 down -v 前,必须确认项目名、最终配置、目标卷、备份和恢复能力。它不是普通“彻底停止”命令。

9.1 restart不会重新读取所有配置

docker compose restart 主要重启已有容器,不等于按新 Compose 模型重建。修改环境变量、挂载、端口或镜像配置后,通常需要 up -d 触发重建,而不是只执行 restart。

验证配置是否真正进入容器:

bash
docker compose config
docker compose up -d
docker inspect <>

十、runexec不是一回事

在已有 api 容器中执行命令:

bash
docker compose exec api sh

exec 要求服务容器正在运行,它在该容器中创建额外进程,共享其文件系统、网络和挂载。

根据 service 配置创建一次性新容器:

bash
docker compose run --rm api java -version

run 会创建新的 one-off 容器,默认行为与端口发布等细节可能不同,应显式检查需要的选项。例如一次性数据库迁移:

bash
docker compose run --rm migration

不要把 run 当成“进入当前线上容器”,否则观察到的 PID、可写层和临时文件不是原实例现场。

十一、服务发现与网络原理

默认情况下,Compose 为项目创建默认 bridge 网络,并将服务连接进去:

yaml
services:
  api:
    image: order-api:1.0
  mysql:
    image: mysql:8.0

api 访问 MySQL 应使用:

text
mysql:3306

而不是:

text
localhost:3306

因为每个容器拥有独立 Network Namespace,localhost 只指向当前容器自己。

11.1 服务名为什么能解析

Compose 将网络端点和服务名/别名交给 Docker 网络系统。连接到同一用户自定义网络的容器,可以通过 Docker 内置 DNS 解析服务名。

mermaid
flowchart TD
    A["api查询mysql"] --> B["容器DNS配置"]
    B --> C["Docker内置DNS"]
    C --> D["找到mysql服务网络端点"]
    D --> E["返回当前容器IP"]
    E --> F["api连接mysql容器3306"]

容器重建后 IP 可能变化,但服务名可以继续解析到新端点。因此客户端不应缓存容器 IP 到永久配置。

11.2 自定义前后端网络

yaml
services:
  gateway:
    image: nginx:1.27
    networks:
      - frontend
      - backend

  api:
    image: order-api:1.0
    networks:
      - backend

  mysql:
    image: mysql:8.0
    networks:
      - backend

networks:
  frontend:
  backend:
    internal: true

internal: true 可限制该网络的外部连通能力,但不能替代应用认证、数据库账号权限和宿主机防火墙。gateway 同时连接两个网络,用于把入口请求转给 backend 中的 api。

11.3 网络别名

yaml
services:
  api:
    image: order-api:1.0
    networks:
      backend:
        aliases:
          - order.internal

networks:
  backend:

别名只在对应网络范围内有意义。不要误认为它会自动注册到企业 DNS 或公网 DNS。

十二、portsexpose与容器内部端口

yaml
services:
  api:
    ports:
      - "127.0.0.1:18080:8080"

含义:

  • 应用在容器内监听 8080。
  • Docker 将宿主机 127.0.0.1:18080 转发到容器 8080。
  • 只有宿主机本地可以直接通过该绑定地址访问。

如果写:

yaml
ports:
  - "18080:8080"

通常绑定所有宿主机接口,是否能从外部访问还受宿主机防火墙、云安全组和路由影响。

expose

yaml
expose:
  - "8080"

用于表达服务内部端口,不会创建宿主机端口映射。同网络容器原本就可以直接访问目标容器实际监听端口,expose 不是网络 ACL。

数据库仅供 api 使用时,通常没必要发布到宿主机:

yaml
services:
  mysql:
    image: mysql:8.0
    # 不配置ports,api仍可通过mysql:3306访问

这样可以减少宿主机暴露端口和端口冲突。

十三、depends_on为什么不能代替应用重试

短语法:

yaml
services:
  api:
    depends_on:
      - mysql

主要表达启动顺序:先启动 mysql 容器,再启动 api 容器。但“mysql 容器主进程已经启动”不等于“MySQL 已经完成初始化并能接受业务连接”。

13.1 健康依赖

yaml
services:
  mysql:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
      interval: 5s
      timeout: 3s
      retries: 20
      start_period: 30s

  api:
    image: order-api:1.0
    depends_on:
      mysql:
        condition: service_healthy

常见条件:

条件含义
service_started依赖容器已启动,不保证应用可服务
service_healthy等待依赖健康检查通过
service_completed_successfully等待一次性任务成功结束,适合迁移任务等场景

具体条件和附加字段支持与 Compose 版本有关,应通过 docker compose versiondocker compose config 验证。

13.2 为什么有healthcheck仍要应用重试

健康依赖只解决初始启动的一部分问题。运行过程中仍可能发生:

  • MySQL 重启。
  • 网络瞬时抖动。
  • DNS 更新。
  • 主从切换。
  • 连接池中的旧连接失效。
  • Redis、MQ 或第三方接口短暂不可用。

应用必须设置连接超时、有限重试、指数退避、熔断和业务降级。无限快速重试会在依赖恢复前制造重试风暴。

13.3 健康检查不能只看进程存在

一个 Java 进程可能仍在运行,但已经:

  • Full GC。
  • 线程池耗尽。
  • 数据库连接池耗尽。
  • 核心配置加载失败。
  • 关键业务初始化未完成。

健康检查应选择低成本、语义明确的 endpoint,并区分“进程活着”和“可以接流量”。检查工具必须真实存在于镜像中,不能照抄 curl 后才发现 JRE 镜像没有 curl。

十四、重启策略与依赖重启不是一回事

yaml
services:
  api:
    restart: unless-stopped

常见策略:

策略行为
no默认,不自动重启
on-failure非零退出时重启,可按环境限制次数
always进程退出后持续尝试重启
unless-stopped除非显式停止,否则在 daemon 重启等情况下恢复

重启策略不能修复应用启动失败。配置错误时 always 可能造成高频重启和日志刷屏,应配合:

bash
docker compose ps
docker inspect --format '{{.RestartCount}} {{json .State}}' <>
docker compose logs --tail 200 api

depends_on 长语法中的 restart: true 描述的是某些 Compose 显式更新依赖后的联动行为,不等同于容器运行时 restart: always,也不应假定依赖每次崩溃重启都会自动重启所有调用方。

十五、Volume生命周期

15.1 命名卷

yaml
services:
  mysql:
    image: mysql:8.0
    volumes:
      - mysql-data:/var/lib/mysql

volumes:
  mysql-data:

通常创建带项目名前缀的命名卷。docker compose down 默认保留它,down -v 可能删除。

15.2 Bind Mount

yaml
services:
  api:
    volumes:
      - type: bind
        source: ./config
        target: /app/config
        read_only: true

相对路径依赖 Compose 项目文件位置和多文件基准规则。Bind Mount 还受宿主机目录是否存在、UID/GID、SELinux 和 Docker Desktop 文件共享影响。

15.3 外部卷

yaml
volumes:
  mysql-data:
    external: true
    name: order-mysql-prod

Compose 不负责创建一个不存在的 external volume,项目删除时也不会把它当作普通项目卷删除。它仍不是备份,也不自动高可用。

15.4 挂载为什么遮住镜像文件

若镜像 /app/config 已有默认文件,再把宿主机空目录挂到同一路径,容器看到的是挂载目录,原镜像文件被遮住。这不是文件被 Compose 删除,而是 Mount 改变了该路径的可见文件系统。

详细原理见 Docker存储挂载与数据生命周期

十六、Secret与Config的真实边界

本地 Compose Secret 示例:

yaml
services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql-root-password
    secrets:
      - mysql-root-password

secrets:
  mysql-root-password:
    file: ./secrets/mysql-root-password.txt

容器中通常以文件形式出现:

text
/run/secrets/mysql-root-password

优点是密码不直接写进 Compose YAML 和普通容器环境变量。但要正确理解:

  • 本地源文件仍需要安全保存和权限控制。
  • 普通本地 Compose 不会因为写了 secrets 就自动获得企业级密钥加密、轮换和审计。
  • 目标镜像或应用必须支持从文件读取,例如 MySQL 官方镜像常见 _FILE 变量。
  • Secret 不能提交到 Git。
  • 不能在启动日志中打印 Secret 内容。

非敏感配置可以使用 configs 或只读 Bind Mount。生产密钥更适合 Vault、云 Secret Manager、Kubernetes Secret 配合加密和 RBAC 等受控系统。

十七、多文件覆盖与合并

基础文件:

yaml
# compose.yaml
services:
  api:
    image: "order-api:${APP_VERSION}"
    environment:
      SPRING_PROFILES_ACTIVE: base
    networks:
      - backend

networks:
  backend:

开发覆盖:

yaml
# compose.dev.yaml
services:
  api:
    environment:
      SPRING_PROFILES_ACTIVE: dev
    ports:
      - "8080:8080"

运行:

bash
docker compose -f compose.yaml -f compose.dev.yaml config
docker compose -f compose.yaml -f compose.dev.yaml up -d

不要把合并简单理解为文本覆盖。不同字段可能:

  • 按映射键合并。
  • 追加或去重序列。
  • 用后一个值替换前一个值。
  • commandentrypoint 等采用特定规则。

另外,多文件中的相对路径通常以基础 Compose 文件的目录作为重要基准,不一定相对每个覆盖文件自身。最可靠方法仍是查看:

bash
docker compose -f compose.yaml -f compose.dev.yaml config

17.1 为什么覆盖文件可能让端口重复

如果基础文件和覆盖文件都追加了端口,而不是得到预期替换,最终模型可能包含多个端口映射。不要只看第二个文件,要看合并结果。

17.2 YAML锚点只做YAML复用

yaml
x-common: &common
  restart: unless-stopped
  logging:
    driver: json-file
    options:
      max-size: "20m"
      max-file: "5"

services:
  api:
    <<: *common
    image: order-api:1.0

锚点是 YAML 层的复用机制,不等于 Compose 服务继承。过度嵌套锚点会让最终配置难以审查,仍应以 docker compose config 展开检查。

十八、Profiles管理可选服务

yaml
services:
  api:
    image: order-api:1.0

  adminer:
    image: adminer:4
    profiles:
      - debug
    ports:
      - "18081:8080"

默认启动:

bash
docker compose up -d

启用调试服务:

bash
docker compose --profile debug up -d

适合:

  • 本地数据库管理工具。
  • 调试代理。
  • 一次性数据准备工具。
  • 可选监控组件。

不要用 Profile 隐藏关键安全控制,也不要让不同环境启用组合完全不可追踪。

十九、Build、Image与Pull策略

yaml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
      target: runtime
      args:
        GIT_COMMIT: "${GIT_COMMIT:?required}"
    image: "registry.example.com/order/order-api:${APP_VERSION:?required}"

这里:

  • build 描述如何构建。
  • image 描述构建结果标签或要使用的镜像引用。
  • 是否构建、是否拉取以及优先行为还受命令选项和 pull policy 影响。

常用流程:

bash
docker compose build --pull
docker compose push
docker compose pull
docker compose up -d

生产环境更常见的是 CI 构建并推送不可变镜像,部署节点只拉取已经测试和扫描的镜像,而不是在生产服务器临时从源码构建。

二十、资源限制与日志

本地 Compose 常见:

yaml
services:
  api:
    cpus: 1.5
    mem_limit: 1g
    pids_limit: 300
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "5"

不同 Compose/Engine 版本对 deploy.resources 等字段在非 Swarm 模式下的支持历史上存在差异。不要因为 YAML 能解析就假定 cgroup 限制已生效,要检查最终容器:

bash
docker compose up -d
docker inspect --format 'memory={{.HostConfig.Memory}} nanoCpus={{.HostConfig.NanoCpus}} pids={{.HostConfig.PidsLimit}}' <>
docker stats --no-stream

日志方面:

  • 应用优先输出 stdout/stderr。
  • 配置 Docker 日志驱动轮转。
  • 不要让无限增长的 json-file 写满 Docker 数据目录。
  • 生产应把日志采集到集中系统,并附带项目、服务、实例和 traceId。

二十一、扩容的能力与边界

bash
docker compose up -d --scale api=3

前提:

  • 不要硬编码同一个 container_name
  • 不要给每个副本绑定同一个宿主机固定端口。
  • 应用状态要外置或支持多实例。
  • 调用方必须能发现多个实例或通过反向代理进入。

普通 Compose 扩展多个容器,不等于完整的集群调度和高可用:

  • 通常仍在单台 Docker 主机。
  • 宿主机宕机后所有副本一起消失。
  • 没有等同 Kubernetes Deployment 的滚动发布控制。
  • 没有完整跨节点调度、自愈、Service 和存储编排语义。

因此“副本数是 3”不等于系统具有三节点容灾。

二十二、可运行商业Demo:Spring Boot、MySQL、Redis

目录:

text
order-stack/
├── compose.yaml
├── .env.example
├── .env
└── order-api/
    ├── Dockerfile
    ├── pom.xml
    └── src/

22.1 本地学习用.env

text
APP_VERSION=dev
APP_PORT=8080
MYSQL_DATABASE=order_db
MYSQL_USER=order_app
MYSQL_PASSWORD=local_only_password
MYSQL_ROOT_PASSWORD=local_only_root_password

提交仓库的是 .env.example,真实 .env 加入 .gitignore。这个做法只适合本地学习和受控开发环境,不是生产密钥方案。

22.2 完整compose.yaml

yaml
name: order-stack

services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: "${MYSQL_DATABASE:?MYSQL_DATABASE is required}"
      MYSQL_USER: "${MYSQL_USER:?MYSQL_USER is required}"
      MYSQL_PASSWORD: "${MYSQL_PASSWORD:?MYSQL_PASSWORD is required}"
      MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD is required}"
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_0900_ai_ci
    volumes:
      - mysql-data:/var/lib/mysql
    networks:
      - backend
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
      interval: 5s
      timeout: 3s
      retries: 20
      start_period: 30s
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "5"

  redis:
    image: redis:7.2
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis-data:/data
    networks:
      - backend
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "5"

  api:
    build:
      context: ./order-api
      dockerfile: Dockerfile
    image: "order-api:${APP_VERSION:-dev}"
    environment:
      SPRING_PROFILES_ACTIVE: compose
      SPRING_DATASOURCE_URL: "jdbc:mysql://mysql:3306/${MYSQL_DATABASE}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"
      SPRING_DATASOURCE_USERNAME: "${MYSQL_USER}"
      SPRING_DATASOURCE_PASSWORD: "${MYSQL_PASSWORD}"
      SPRING_DATA_REDIS_HOST: redis
      SPRING_DATA_REDIS_PORT: "6379"
    ports:
      - "127.0.0.1:${APP_PORT:-8080}:8080"
    networks:
      - backend
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    stop_grace_period: 30s
    mem_limit: 1g
    cpus: 1.5
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "5"

  adminer:
    image: adminer:4
    profiles:
      - debug
    ports:
      - "127.0.0.1:18081:8080"
    networks:
      - backend
    depends_on:
      mysql:
        condition: service_healthy

networks:
  backend:
    internal: false

volumes:
  mysql-data:
  redis-data:

backend.internal 在本地 Demo 中设为 false,因为构建和运行环境可能需要正常外部访问;生产网络边界应按入口、依赖和运维需求单独设计,不能机械改成 true。

22.3 启动前验证

powershell
Copy-Item .env.example .env
docker compose config --quiet
docker compose config
docker compose build
docker compose up -d --wait

Linux/macOS 复制命令:

bash
cp .env.example .env

22.4 启动后验证

bash
docker compose ps
docker compose images
docker compose top
docker compose logs --tail 200 mysql
docker compose logs --tail 200 redis
docker compose logs --tail 200 api
curl -fsS http://127.0.0.1:8080/actuator/health

确认服务名解析:

bash
docker compose exec api getent hosts mysql
docker compose exec api getent hosts redis

极简 Java 镜像可能没有 getentcurl 或 Shell。工具不存在不等于网络失败,可以:

  • 从同网络启动临时诊断容器。
  • 使用应用自身连接日志和指标。
  • 在受控调试镜像中安装工具。
  • 从宿主机检查 Docker 网络对象。

临时诊断容器示例:

bash
docker run --rm --network order-stack_backend nicolaka/netshoot \
  sh -c 'getent hosts mysql && nc -vz mysql 3306 && getent hosts redis && nc -vz redis 6379'

生产使用第三方诊断镜像前应先固定版本、扫描来源并遵守安全策略。

22.5 停止与数据验证

bash
docker compose down
docker volume ls --filter name=order-stack
docker compose up -d --wait

命名卷仍在,因此 MySQL 和 Redis 数据应继续存在。

不要在有价值数据环境随意执行:

bash
docker compose down -v

22.6 从本地Demo演进到生产还缺什么

  • 密码改为受控 Secret 系统,不使用普通 .env
  • 镜像由 CI 构建、测试、扫描、签名并推送仓库。
  • 部署按不可变 Digest,而不是开发标签。
  • MySQL 和 Redis 使用受管服务或有备份、复制、故障恢复的独立架构。
  • 日志、指标和链路追踪进入集中平台。
  • 配置变更、发布、回滚和权限有审计。
  • 多主机高可用、滚动发布和弹性伸缩交给更合适的编排平台。

二十三、常见故障排查

23.1 docker compose up提示变量缺失

bash
docker compose config --environment
docker compose config

检查:

  • 当前 Shell 是否设置变量。
  • .env 是否位于 Compose 预期目录。
  • 是否使用了正确 --env-file
  • PowerShell、Bash 的变量设置语法是否混用。
  • ${VAR:?message} 是否按设计阻止缺失值。

23.2 修改环境变量后没有生效

不要只执行:

bash
docker compose restart api

先检查并重建:

bash
docker compose config
docker compose up -d api
docker inspect --format '{{json .Config.Env}}' <api容>

还要确认 Spring Boot 实际读取的属性名和配置优先级。

23.3 api仍然连不上mysql

按层排查:

mermaid
flowchart TD
    A["api连接mysql失败"] --> B["检查最终连接地址"]
    B --> C{"是否错误使用localhost"}
    C -- "是" --> D["改用服务名mysql与容器端口3306"]
    C -- "否" --> E["检查两个服务是否在同一网络"]
    E --> F["检查DNS解析"]
    F --> G["检查MySQL健康与监听"]
    G --> H["检查账号、密码、库名和权限"]
    H --> I["检查连接超时、重试和连接池日志"]

命令:

bash
docker compose ps
docker compose config
docker compose logs --tail 200 mysql
docker compose logs --tail 200 api
docker network inspect order-stack_backend

23.4 MySQL容器一直Restarting

bash
docker compose ps
docker compose logs --tail 300 mysql
docker inspect --format '{{json .State}}' <mysql容>
docker inspect --format '{{json .Mounts}}' <mysql容>

常见原因:

  • 必填密码变量缺失。
  • 数据目录权限不对。
  • 数据文件版本与 MySQL 镜像版本不兼容。
  • 磁盘已满。
  • 配置参数错误。
  • 健康检查失败被误认为容器进程退出;两者需要分开看。

23.5 port is already allocated

宿主机端口只能被一个监听者占用:

bash
docker ps --format 'table {{.Names}}\t{{.Ports}}'
docker compose ps

解决方向:

  • 更换宿主机端口,而不是容器内部服务端口。
  • 删除真正不再使用的旧容器。
  • 数据库只供容器使用时取消 ports
  • 检查是否启动了另一个项目名相同或不同的 Compose 栈。

23.6 出现孤儿容器

从 Compose 文件删除或改名服务后,旧项目容器可能仍存在。先确认项目:

bash
docker compose ls
docker ps --filter label=com.docker.compose.project=order-stack
docker compose up -d --remove-orphans

--remove-orphans 会删除不在当前模型中的同项目容器,使用多个 Profile 或覆盖文件时必须确认它不是仍有用途的服务。

23.7 down后数据“丢失”

检查:

bash
docker compose config
docker compose ls
docker volume ls
docker inspect --format '{{json .Mounts}}' <mysql容>

可能原因:

  • 使用匿名卷,后来挂到了新卷。
  • 项目名改变,创建了新前缀命名卷。
  • 执行过 down -v
  • 执行了 volume prune
  • Bind Mount 指向了错误宿主机目录。
  • 应用原本写在容器可写层而非 Volume。

23.8 服务状态Healthy但业务仍失败

健康检查只证明测试命令成功,不证明所有业务链路正常。继续检查:

  • 健康 endpoint 是否过于简单。
  • API 是否能访问数据库、Redis、MQ 和第三方接口。
  • 线程池、连接池是否耗尽。
  • 业务权限和初始化数据是否正确。
  • 请求错误率和 P99,而不只是容器状态。

二十四、Compose适不适合生产

不能只回答“可以”或“不可以”。需要看系统边界。

适合

  • 本地开发和集成测试。
  • CI 临时依赖环境。
  • 单机内部工具。
  • 小规模、可接受单机故障的服务。
  • 边缘节点或受控设备上的固定应用栈。

不足

  • 普通 Compose 主要面向单 Docker Engine,宿主机是单点。
  • 缺少完整跨节点调度。
  • 没有 Kubernetes Deployment 等价的声明式滚动发布能力。
  • 多副本服务发现、负载均衡和故障迁移能力有限。
  • Secret、RBAC、策略、审计和多租户能力需要外部系统。
  • 有状态服务高可用和存储故障恢复需要单独设计。

选型思路

场景更常见选择
开发机启动依赖Docker Compose
CI集成测试环境Compose或测试容器方案
单机内部系统Compose,加备份、监控和严格运维
Docker原生集群编排Swarm,需结合团队现状评估
多节点微服务、滚动发布、自愈Kubernetes或企业容器平台
云上数据库、Redis优先评估受管服务

工具不是架构保证。即使使用 Kubernetes,如果没有备份、限流、观测和故障演练,也不能自动得到可靠系统。

二十五、面试标准回答

Docker Compose工作原理

Compose读取一个或多个Compose文件,完成变量插值、配置合并和校验,生成包含services、networks、volumes、secrets等对象的项目模型。随后比较期望模型与当前项目资源,通过Docker Engine API拉取或构建镜像、创建网络和Volume、创建或重建容器,并按依赖关系启动。容器最终仍由Docker daemon、containerd和OCI runtime运行,Compose本身不是新的容器运行时。

docker compose upstart区别

up根据当前配置协调项目,缺少资源时会创建,配置或镜像变化时可能重建容器;start只启动已经存在且停止的容器,不重新按新配置创建。修改环境变量、端口、挂载后只执行start或restart通常不会得到新配置,应先用docker compose config确认,再执行up -d并inspect最终容器。

depends_on为什么不能保证依赖可用

短语法主要保证启动顺序,依赖容器主进程启动不等于数据库已完成初始化。可以为依赖定义healthcheck并使用condition: service_healthy改善初始启动,但运行中仍可能发生重启、网络抖动和主从切换,所以应用还必须设置连接超时、有限重试、退避、熔断和降级。

.envenv_file区别

项目.env主要为Compose解析YAML时的${VAR}插值提供值,也可能影响CLI行为;service的env_file用于把变量注入指定容器。一个变量存在于.env不表示它自动进入容器。应先用docker compose config检查解析模型,再用docker inspect检查容器实际Env。

downdown -v区别

down删除项目容器和默认网络,普通命名卷默认保留;down -v还会删除项目声明的相关卷,数据库数据可能因此丢失。Volume只提供独立于容器的生命周期,不等于备份,生产执行前必须核对项目名、最终配置、实际Mounts、目标卷和恢复能力。

Compose为什么不应滥用container_name

Compose原本通过项目名、服务名和实例序号隔离资源,服务名还承担网络DNS名称。固定container_name容易造成跨项目冲突,并阻碍同一服务扩展多个副本。调用方应依赖服务发现名称而不是某个具体实例名或容器IP。

Compose是否适合生产

Compose适合开发、测试、CI临时环境以及可接受单机边界的小规模部署。普通Compose主要协调单个Docker Engine,不提供完整跨节点调度、滚动发布、自愈、多租户和集群存储编排。若系统要求多节点高可用、弹性扩缩和标准化发布,通常使用Kubernetes等平台;即使单机使用Compose,也必须补备份、监控、密钥、回滚和故障恢复。

二十六、学习验收

完成学习后,应能不看答案解释并演示:

  1. 从两个 Compose 文件推导最终 docker compose config
  2. 区分 Compose 插值值和容器最终环境变量。
  3. 解释项目名变化为什么会创建另一组容器、网络和 Volume。
  4. 解释服务名 DNS 和容器独立 localhost。
  5. 说明 upstartrestartrunexecdown 的对象变化。
  6. 用 healthcheck 与 condition 控制初始顺序,同时说明为什么仍要应用重试。
  7. 让 MySQL 数据在普通 down/recreate 后仍保留,并证明 down -v 的风险。
  8. configpslogsinspectnetwork inspectvolume inspect 完成故障定位。
  9. 说明环境变量、Secret 文件和企业密钥系统的安全边界。
  10. 判断给定系统应使用 Compose 还是集群编排平台,并给出故障域理由。

关联知识点