docker/compose · error

process still running

Error message

process still running

What it means

After a sync_exec watch action streams the exec session to completion, Compose inspects the exec to fetch its exit code. If ExecInspect still reports Running=true at that point, the exec process never terminated, which would leave the sync step in an ambiguous state, so it errors with 'process still running'.

Solutions

  1. Make the sync_exec command finite: ensure it runs to completion and exits (no servers, no tail -f)
  2. Background or daemonize inside the container yourself if you need a long-running process, and let the exec command return immediately
  3. If it looks like a transient race, simply retry the watch — the container state is not corrupted

Example fix

# before
- action: sync_exec
  path: ./src
  target: /app
  exec:
    command: npm run dev   # never exits
# after
- action: sync_exec
  path: ./src
  target: /app
  exec:
    command: sh -c "npm install && npm run build"
Defensive patterns

Strategy: retry

Validate before calling

docker compose exec <svc> sh -c 'command -v my-one-shot-cmd >/dev/null && my-one-shot-cmd --check'

Try / catch

# transient race retry: re-running watch re-runs sync_exec
for i in 1 2 3; do docker compose watch & sleep 5; kill %1 2>/dev/null; done

Prevention

When it happens

Trigger: A develop.watch rule with action: sync_exec whose command is long-running or daemonizing (e.g. a server, tail -f, or a script that backgrounds itself), so the attach streams finish but the exec stays alive.

Common situations: Using sync_exec to run build/test commands that spawn daemons; commands like `npm run dev` that never exit; a race where a slow-starting process has not exited when inspected.

Related errors


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

Appendix: source

Thrown at pkg/compose/watch.go:515

		Detach: false,
	})
	if err != nil {
		return err
	}

	// although the errgroup is not tied directly to the context, the operations
	// in it are reading/writing to the connection, which is tied to the context,
	// so they won't block indefinitely
	if err := eg.Wait(); err != nil {
		return err
	}

	execResult, err := t.s.apiClient().ExecInspect(ctx, execCreateResp.ID, client.ExecInspectOptions{})
	if err != nil {
		return err
	}
	if execResult.Running {
		return errors.New("process still running")
	}
	if execResult.ExitCode != 0 {
		return fmt.Errorf("exit code %d", execResult.ExitCode)
	}
	return nil
}

func (t tarDockerClient) Untar(ctx context.Context, id string, archive io.ReadCloser) error {
	_, err := t.s.apiClient().CopyToContainer(ctx, id, client.CopyToContainerOptions{
		DestinationPath: "/",
		Content:         archive,
		CopyUIDGID:      true,
	})
	return err
}

//nolint:gocyclo
func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Project, options api.WatchOptions, batch []watch.FileEvent, rules []watchRule, syncer sync.Syncer) error {

View on GitHub (pinned to ddc4b044b6)