{"record":{"id":"75ba533e9c8bc310","repo":"docker/compose","slug":"watch-rules-must-define-a-path","errorCode":null,"errorMessage":"watch rules MUST define a path","messagePattern":"watch rules MUST define a path","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/compose/watch.go","lineNumber":421,"sourceCode":"\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbaseDir, err := filepath.EvalSymlinks(project.WorkingDir)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"resolving symlink for %q: %w\", project.WorkingDir, err)\n\t}\n\n\tfor i, trigger := range config.Watch {\n\t\tif !filepath.IsAbs(trigger.Path) {\n\t\t\ttrigger.Path = filepath.Join(baseDir, trigger.Path)\n\t\t}\n\t\tif p, err := filepath.EvalSymlinks(trigger.Path); err == nil {\n\t\t\t// this might fail because the path doesn't exist, etc.\n\t\t\ttrigger.Path = p\n\t\t}\n\t\ttrigger.Path = filepath.Clean(trigger.Path)\n\t\tif trigger.Path == \"\" {\n\t\t\treturn nil, errors.New(\"watch rules MUST define a path\")\n\t\t}\n\n\t\tif trigger.Action == types.WatchActionRebuild && service.Build == nil {\n\t\t\treturn nil, fmt.Errorf(\"service %s doesn't have a build section, can't apply %s on watch\", types.WatchActionRebuild, service.Name)\n\t\t}\n\t\tif trigger.Action == types.WatchActionSyncExec && len(trigger.Exec.Command) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"can't watch with action %q on service %s without a command\", types.WatchActionSyncExec, service.Name)\n\t\t}\n\n\t\tconfig.Watch[i] = trigger\n\t}\n\treturn &config, nil\n}\n\nfunc checkIfPathAlreadyBindMounted(watchPath string, volumes []types.ServiceVolumeConfig) bool {\n\tfor _, volume := range volumes {\n\t\tif volume.Bind != nil {\n\t\t\trelPath, err := filepath.Rel(volume.Source, watchPath)","sourceCodeStart":403,"sourceCodeEnd":439,"githubUrl":"https://github.com/docker/compose/blob/ddc4b044b62e9f715212ea4143fa830fac76382f/pkg/compose/watch.go#L403-L439","documentation":"Before watching, Compose normalizes each develop.watch rule's path: relative paths are joined with the project base dir, symlinks evaluated, and the result cleaned. If the resulting path is the empty string — i.e. the rule effectively defines no path — the config is rejected because the file watcher has nothing to observe.","triggerScenarios":"A develop.watch rule whose path resolves to empty: typically `path:` omitted entirely (empty struct field) when baseDir is also empty, or a template-generated compose file emitting `path: \"\"`.","commonSituations":"YAML anchors or env interpolation producing an empty path (${WATCH_PATH} unset); hand-writing a watch rule and forgetting the path key; machine-generated compose files with sparse watch configs.","solutions":["Add an explicit `path:` to the watch rule (relative to the service build context or absolute)","If the path comes from interpolation, default it: `path: ${WATCH_PATH:-./src}`","Remove rules you do not need instead of leaving them with empty fields"],"exampleFix":"# before\nservices:\n  web:\n    develop:\n      watch:\n        - action: sync\n          path: \"\"\n          target: /app\n# after\nservices:\n  web:\n    develop:\n      watch:\n        - action: sync\n          path: ./src\n          target: /app","handlingStrategy":"validation","validationCode":"python3 - <<'EOF'\nimport yaml,sys\ncfg=yaml.safe_load(open('compose.yaml'))\nfor n,s in (cfg.get('services') or {}).items():\n    for r in ((s.get('develop') or {}).get('watch') or []):\n        if not r.get('path'): sys.exit(f\"service {n}: watch rule missing path\")\nprint('ok')\nEOF","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Default interpolated paths: path: ${WATCH_PATH:-./src}","Validate with `docker compose config` after template-rendering compose files"],"tags":["watch","develop","compose-file","validation"],"backgroundTag":null,"analyzedSha":"ddc4b044b62e9f715212ea4143fa830fac76382f","analyzedAt":"2026-08-15T13:31:42.319Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}