docker/compose · error
service %s doesn't have a build section, can't apply %s on w
Error message
service %s doesn't have a build section, can't apply %s on watch
What it means
Thrown while Compose validates a service's `develop.watch` rules: a trigger uses `action: rebuild`, but the service has no `build:` section, so there is nothing to rebuild. The format arguments are swapped in the source, so the rendered message reads like `service rebuild doesn't have a build section, can't apply web on watch` — the first %s is actually the action and the second is the service name.
Source
Thrown at pkg/compose/watch.go:425
if err != nil {
return nil, fmt.Errorf("resolving symlink for %q: %w", project.WorkingDir, err)
}
for i, trigger := range config.Watch {
if !filepath.IsAbs(trigger.Path) {
trigger.Path = filepath.Join(baseDir, trigger.Path)
}
if p, err := filepath.EvalSymlinks(trigger.Path); err == nil {
// this might fail because the path doesn't exist, etc.
trigger.Path = p
}
trigger.Path = filepath.Clean(trigger.Path)
if trigger.Path == "" {
return nil, errors.New("watch rules MUST define a path")
}
if trigger.Action == types.WatchActionRebuild && service.Build == nil {
return nil, fmt.Errorf("service %s doesn't have a build section, can't apply %s on watch", types.WatchActionRebuild, service.Name)
}
if trigger.Action == types.WatchActionSyncExec && len(trigger.Exec.Command) == 0 {
return nil, fmt.Errorf("can't watch with action %q on service %s without a command", types.WatchActionSyncExec, service.Name)
}
config.Watch[i] = trigger
}
return &config, nil
}
func checkIfPathAlreadyBindMounted(watchPath string, volumes []types.ServiceVolumeConfig) bool {
for _, volume := range volumes {
if volume.Bind != nil {
relPath, err := filepath.Rel(volume.Source, watchPath)
if err == nil && !strings.HasPrefix(relPath, "..") {
return true
}
}View on GitHub (pinned to ddc4b044b6)
Solutions
- Add a `build:` section to the service so the rebuild action has a build context
- Change the watch trigger action from `rebuild` to `sync` if the service runs a prebuilt image
- Remove the rebuild watch rule for image-only services
Example fix
# before
services:
web:
image: nginx:alpine
develop:
watch:
- path: ./src
action: rebuild
# after
services:
web:
build: ./src
develop:
watch:
- path: ./src
action: rebuild Defensive patterns
Strategy: validation
Validate before calling
# before running docker compose watch, assert every rebuild rule has a build section
python3 - <<'EOF'
import yaml, sys
p = yaml.safe_load(open('compose.yaml'))
for name, svc in (p.get('services') or {}).items():
for rule in ((svc.get('develop') or {}).get('watch') or []):
if rule.get('action') == 'rebuild' and not svc.get('build'):
sys.exit(f"service {name}: rebuild watch rule but no build section")
EOF Prevention
- Keep watch rules next to the build section they depend on when authoring services
- Prefer `sync` for image-only services; reserve `rebuild` for services with a build context
- Lint compose files in CI with `docker compose config` before deploying watch-based workflows
When it happens
Trigger: Running `docker compose watch` (or `up --watch`) on a project where a service declares `develop: watch: [{action: rebuild, ...}]` but only defines `image:` with no `build:` block. Raised during config load in pkg/compose/watch.go before any watcher starts.
Common situations: Copy-pasting a watch section from a template into a service that pulls a prebuilt image; converting a build-based service to `image:` and forgetting to change the watch action to `sync`; using `rebuild` where `sync` + `exec` was intended.
Related errors
- none of the selected services is configured for watch, see h
- none of the selected services is configured for watch, consi
- can't watch with action %q on service %s without a command
- watch rules MUST define a path
- --wait cannot be combined with --abort-on-container-exit, --
AI-assisted analysis of docker/compose@ddc4b044b6 (2026-08-15).
Data as JSON: /api/errors/26bf95d219e77e87.
Report an issue: GitHub.