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

  1. Add a `build:` section to the service so the rebuild action has a build context
  2. Change the watch trigger action from `rebuild` to `sync` if the service runs a prebuilt image
  3. 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

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


AI-assisted analysis of docker/compose@ddc4b044b6 (2026-08-15). Data as JSON: /api/errors/26bf95d219e77e87. Report an issue: GitHub.