{"record":{"id":"33e42fd768c07a15","repo":"docker/compose","slug":"process-still-running","errorCode":null,"errorMessage":"process still running","messagePattern":"process still running","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/compose/watch.go","lineNumber":515,"sourceCode":"\t\tDetach: false,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// although the errgroup is not tied directly to the context, the operations\n\t// in it are reading/writing to the connection, which is tied to the context,\n\t// so they won't block indefinitely\n\tif err := eg.Wait(); err != nil {\n\t\treturn err\n\t}\n\n\texecResult, err := t.s.apiClient().ExecInspect(ctx, execCreateResp.ID, client.ExecInspectOptions{})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif execResult.Running {\n\t\treturn errors.New(\"process still running\")\n\t}\n\tif execResult.ExitCode != 0 {\n\t\treturn fmt.Errorf(\"exit code %d\", execResult.ExitCode)\n\t}\n\treturn nil\n}\n\nfunc (t tarDockerClient) Untar(ctx context.Context, id string, archive io.ReadCloser) error {\n\t_, err := t.s.apiClient().CopyToContainer(ctx, id, client.CopyToContainerOptions{\n\t\tDestinationPath: \"/\",\n\t\tContent:         archive,\n\t\tCopyUIDGID:      true,\n\t})\n\treturn err\n}\n\n//nolint:gocyclo\nfunc (s *composeService) handleWatchBatch(ctx context.Context, project *types.Project, options api.WatchOptions, batch []watch.FileEvent, rules []watchRule, syncer sync.Syncer) error {","sourceCodeStart":497,"sourceCodeEnd":533,"githubUrl":"https://github.com/docker/compose/blob/ddc4b044b62e9f715212ea4143fa830fac76382f/pkg/compose/watch.go#L497-L533","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Make the sync_exec command finite: ensure it runs to completion and exits (no servers, no tail -f)","Background or daemonize inside the container yourself if you need a long-running process, and let the exec command return immediately","If it looks like a transient race, simply retry the watch — the container state is not corrupted"],"exampleFix":"# before\n- action: sync_exec\n  path: ./src\n  target: /app\n  exec:\n    command: npm run dev   # never exits\n# after\n- action: sync_exec\n  path: ./src\n  target: /app\n  exec:\n    command: sh -c \"npm install && npm run build\"","handlingStrategy":"retry","validationCode":"docker compose exec <svc> sh -c 'command -v my-one-shot-cmd >/dev/null && my-one-shot-cmd --check'","typeGuard":null,"tryCatchPattern":"# transient race retry: re-running watch re-runs sync_exec\nfor i in 1 2 3; do docker compose watch & sleep 5; kill %1 2>/dev/null; done","preventionTips":["Keep sync_exec commands strictly finite (build, lint, test) — never servers","Long-running processes belong in the service entrypoint, not sync_exec"],"tags":["watch","sync-exec","exec","container"],"backgroundTag":null,"analyzedSha":"ddc4b044b62e9f715212ea4143fa830fac76382f","analyzedAt":"2026-08-15T13:31:42.319Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}