首页
/ Docker Compose CLI 中 watch 命令深度解析:从 alpha watch 参数到 develop.watch 热重载实现

Docker Compose CLI 中 watch 命令深度解析:从 alpha watch 参数到 develop.watch 热重载实现

2026-09-05 22:38:00作者:董宙帆

docker compose alpha watch 是 Compose 项目(Define and run multi-container applications with Docker)提供的文件监视命令参考页:监视服务的构建上下文,在文件更新时自动重建/刷新容器。本文以该参考文档为主线,完整覆盖其命令用法与全部参数,并结合 cmd/compose/watch.gopkg/compose/watch.go 的源码实现,讲清 develop.watch 配置、事件处理与同步/重建流水线,读完后可直接在本地开发流程中落地「改代码即生效」的容器化工作流。

命令定位:alpha watch 已成为顶级命令

参考文档 docs/reference/compose_alpha_watch.md 记录的是实验性子命令入口:

usage: docker compose alpha watch [SERVICE...]

Watch build context for service and rebuild/refresh containers when files are updated

需要注意的是,在当前仓库中 watch 已从实验性命令"转正"为顶级命令,alpha 只是历史入口。源码中有两处直接证据:

  • cmd/compose/compose.gowatchCommandalphaCommand 被并列注册为顶级子命令;
  • cmd/compose/watch.goRunE 中,如果检测到命令是通过 alpha 父命令调用的,会打印一条警告:
if cmd.Parent().Name() == "alpha" {
    logrus.Warn("watch command is now available as a top level command")
}

也就是说,执行 docker compose alpha watch 依然可用,但会提示用户改用 docker compose watch。当前 cmd/compose/alpha.go 中的 alpha 子命令组只保留了 vizpublishgenerate 三个实验性命令,watch 不再属于其中。顶级入口的参考文档见 docs/reference/compose_watch.md

完整参数说明

参考文档列出的选项如下(--dry-run 为从项目选项继承而来的选项):

名称 类型 默认值 说明
--dry-run bool Execute command in dry run mode
--no-up bool Do not build & start services before watching
--quiet bool hide build output

cmd/compose/watch.go 的 flag 注册代码看,还存在一个文档表中未体现的选项 --prune,且在顶级命令中默认开启:

cmd.Flags().BoolVar(&buildOpts.quiet, "quiet", false, "hide build output")
cmd.Flags().BoolVar(&watchOpts.prune, "prune", true, "Prune dangling images on rebuild")
cmd.Flags().BoolVar(&watchOpts.noUp, "no-up", false, "Do not build & start services before watching")

逐项解释:

  • --no-up(默认 false):默认行为是 watch 启动时先执行一次等价于 up 的流程——构建、创建并启动服务,然后才进入监视循环;加上该参数则跳过这步,假定服务已经在运行,直接进入文件监视。
  • --quiet(默认 false):隐藏构建输出。它同时作用于初始 up 阶段的拉取(QuietPull)与后续 rebuild 的构建日志(见 cmd/compose/watch.go)。
  • --dry-run:继承自项目级选项,以干跑模式执行命令。
  • --prune(默认 true):每次重建后清理属于本项目的 dangling 镜像,避免反复 rebuild 导致本地磁盘堆积旧镜像。实现位于 pkg/compose/watch.gopruneDanglingImagesOnRebuild:按项目名过滤出 dangling=true 的镜像,再排除本次新构建出的镜像 ID,其余全部 ImageRemove

命令的参数是可选的服务名列表 [SERVICE...],并支持按服务名自动补全(ValidArgsFunction: completeServiceNames)。

前置条件:develop.watch 配置

watch 命令本身不产生监视规则,规则来自 Compose 文件中的 develop 段(Compose Specification 的 develop.watch 特性)。典型配置如下(取自端到端测试夹具 pkg/e2e/fixtures/watch/compose.yaml):

x-dev: &x-dev
  watch:
    - action: sync
      path: ./data
      target: /app/data
      ignore:
        - '*.foo'
        - ./ignored
    - action: sync+restart
      path: ./config
      target: /app/config

services:
  alpine:
    build:
      dockerfile_inline: |-
        FROM alpine
        RUN mkdir -p /app/data
        RUN mkdir -p /app/config
    init: true
    command: sleep infinity
    develop: *x-dev

各字段与行为对应源码中的处理:

  • path:宿主机上要监视的路径。相对路径会在加载时解析为基于项目工作目录的绝对路径,并做符号链接展开与 Clean,见 pkg/compose/watch.goloadDevelopmentConfig 的处理。
  • target:容器内对应的目标路径。当设置 target 时,宿主机相对路径会被转换成 Unix 风格的容器内相对路径(见 pkg/compose/watch.gowatchRule.Matchesfilepath.Rel + path.Join 逻辑),sync 动作据此把文件写入容器的正确位置。
  • action:触发动作,共 5 种,全部在 pkg/compose/watch.gohandleWatchBatch 中按类型分派:
    • rebuild:重建服务镜像并强制重建(recreate)容器。要求服务必须有 build 上下文,否则在加载阶段就报错 can't watch service ... with action rebuild without a build context(见 cmd/compose/watch.gopkg/compose/watch.go)。
    • sync:把变更文件同步进正在运行的容器,不重启。
    • sync+restart:先同步文件,再重启服务容器(测试夹具中用于配置文件热更新场景)。
    • sync+exec:同步文件后在容器内执行一段 exec.command 命令(多容器服务会在每个容器上并行执行,见 pkg/compose/watch.go)。
    • restart:只重启,不同步文件。
  • include / ignore:glob 过滤。实现上是一个组合匹配器(composite matcher),把 .dockerignore(通过 watch.LoadDockerIgnore(service.Build) 从构建上下文加载)、临时文件匹配器、硬编码的 .git/ 忽略规则、用户 ignore 模式叠加在一起,见 pkg/compose/watch.gogetWatchRules。没有配置 include 时默认使用 AnyMatcher 放行全部文件。
  • initial_sync:声明该 sync 规则在监视启动时先做一次全量初始同步,实现见 pkg/compose/watch.goinitialSync。注意旧的扩展属性 x-initialSync 已被标记为弃用(deprecated),使用它会打印警告并提示改用官方的 initial_sync 属性(见 pkg/compose/watch.go)。
  • x-developdevelop 的旧写法,同样已弃用,加载时会打印 x-develop is DEPRECATED 警告(见 pkg/compose/watch.go)。

rebuild 动作还有一个隐含约束:prepareRebuildTriggerspkg/compose/watch.go)会把含 rebuild 触发器的服务的拉取策略设为 PullPolicyBuild,即永远构建而不是拉取远程镜像。

执行流程:从 runWatch 到监视循环

1. 入口与项目锁

docker compose alpha watch [SERVICE...] 的完整流程入口是 cmd/compose/watch.gorunWatch,依次做四件事:

  1. 加载项目(WithoutEnvironmentResolution),再仅针对选中的服务解析环境变量——这样无关服务声明的 env_file 不存在也不会导致失败;
  2. 通过 internal/locker 的 pidfile 对项目加互斥锁(locker.NewPidfile(project.Name) + Lock()),防止同一项目被多个 compose 进程并发操作;
  3. 若未指定 --no-up,执行一次 UpRecreateDivergedInherit: trueRemoveOrphans: false),把服务先拉起来;
  4. 调用 backend.Watch 进入监视循环,日志通过 formatter.NewLogConsumer 输出到终端。

2. 收集触发路径并启动监视

核心实现在 pkg/compose/watch.gocomposeService.watch

  • 遍历每个服务,解析 develop(或弃用的 x-develop)配置,没有配置 develop 的服务直接跳过;若最终没有任何触发路径,会报错 none of the selected services is configured for watch, consider setting a 'develop' section
  • watchTriggerPathspkg/compose/watch.go)有两个关键判断:
    • 如果 sync 规则的 path 已经被该服务的 bind mount 卷覆盖,则跳过监视并告警path '%s' also declared by a bind mount volume, this path won't be monitored!——因为 bind mount 本身就会实时共享,无需再同步;
    • 如果规则声明了初始同步,则在启动监视前先执行 initialSync
  • 文件事件来自 pkg/watchwatch.NewWatcher(paths),跨平台实现分别在 watcher_nonwin.go / watcher_windows.go / watcher_darwin.go

3. 事件批处理与动作执行

watchEventspkg/compose/watch.go)是一个长期运行的 select 循环:

  • 使用 watch.BatchDebounceEvents 对文件系统事件做去抖与批处理,把 IDE 一次性保存大量文件识别为单个 batch,避免一次保存触发几十次构建;
  • 单批超过 1000 个文件时会打印警告,并提示"如果刚切换了分支,这是预期行为";
  • 每个 batch 交给 handleWatchBatchpkg/compose/watch.go)分派:按规则匹配事件路径后,聚合出 rebuild / syncfiles / restart / exec 四类动作,执行顺序固定为 先 rebuild → 再 sync → 再 restart → 最后并行 exec

4. sync:tar 打包 + CopyToContainer 传输

sync 的具体传输实现由 getSyncImplementation 决定(pkg/compose/watch.go):当前唯一可用实现是 tar 批量传输(可用环境变量 COMPOSE_EXPERIMENTAL_WATCH_TAR=false 关闭,关闭后直接报 no available sync implementation)。其原理是把变更文件打包成 tar 归档,通过 Docker Engine 的 CopyToContainer API(DestinationPath: "/"CopyUIDGID: true)批量写入容器,见 pkg/compose/watch.go;归档打包逻辑在 internal/sync/tar.go

5. rebuild:只重建被监视的服务

rebuild 路径(pkg/compose/watch.go)值得注意两点:

  • 不级联依赖up --build 为了初始启动会构建 depends_on 的依赖镜像(Deps=true),而 watch 触发重建时会显式把 buildOpts.Deps 重置为 falseServices 限定为被改动的服务——否则改一个服务会级联重建整条依赖链。端到端测试 TestWatchRebuildIgnoresDependencies 专门验证了"改 frontend 不会触发 backend 重建";
  • 重建闭环:build → (可选)清理 dangling 镜像 → RecreateForce 强制创建 → start 启动并附加日志。

典型用法示例

结合 e2e 测试(pkg/e2e/watch_test.go)中的真实调用方式,常见用法有三类:

1. 直接启动监视(默认先 up 再 watch)

docker compose alpha watch web     # 历史写法,会提示已转正
docker compose watch web           # 推荐写法

对应夹具 pkg/e2e/fixtures/watch/rebuild.yaml:三个服务 a/b/c 都用 path: test, action: rebuild 监视同一个文件,修改该文件后三个服务会依次重建并生效。

2. 与 up 组合使用

docker compose up --watch
docker compose up --build --watch

e2e 测试中 up --watch 是主要验证入口,--build 用于确保带 build 上下文的镜像在启动与重建时都本地构建。

3. 服务已运行时只开监视

docker compose up -d
docker compose watch --no-up web

--no-up 跳过初始构建与启动,适合服务已由其他流程拉起、只需增量同步文件的场景;配合 --quiet 可压掉构建输出,--prune=false 可在需要保留历史镜像时关闭 dangling 清理。

端到端测试验证的行为边界

pkg/e2e/watch_test.go 中的测试用例界定了该命令的行为边界,可作为验收清单:

  • 文件修改/删除/子目录增删TestWatch 覆盖改内容、删文件、建子目录、删目录等场景,确认容器内状态与宿主机一致(pkg/e2e/watch_test.go);
  • ignore 生效:写入 data.foo(匹配 *.foo)与 ignored/ 目录下的文件不会同步进容器;
  • sync+restart:修改 ./config 下文件后,输出中出现 service(s) [...] restarted 且新内容可读(pkg/e2e/watch_test.go);
  • sync+execTestWatchExec 使用 fixtures/watch/exec.yaml,在文件变更后容器内执行命令并输出结果;
  • rebuild 不级联依赖TestWatchRebuildIgnoresDependencies
  • 符号链接目录TestWatchSyncIntoSymlinkedDirectory 验证同步会穿透镜像内预置的符号链接,而不是覆盖它;
  • include 过滤TestWatchIncludes 验证只有 A.test 被同步、B.test 被过滤。

小结

docker compose alpha watch 参考页描述的命令在当前仓库中已是顶级命令 docker compose watch,参数为 --dry-run--no-up--quiet,源码中另有默认开启的 --prune 用于重建后清理 dangling 镜像。其本质是:基于 Compose 文件中的 develop.watch 规则(rebuild / sync / sync+restart / sync+exec / restart 五种动作),对触发路径做跨平台文件监视,事件经去抖批处理后,通过 tar + CopyToContainer 同步文件或触发"构建—重建—启动"闭环,从而实现容器化开发环境下的实时热重载。相关入口与实现可进一步查看 cmd/compose/watch.gopkg/compose/watch.gopkg/e2e/fixtures/watch/compose.yaml

登录后查看全文
热门项目推荐
相关项目推荐