Skip to content

dev: Container agents still orphan the host port on Ctrl+C / process.exit (exit reaper misses the container) #1714

Description

@tejaskash

Summary

The exit-reaper added in #1695 (registerExitCleanup in src/cli/operations/dev/dev-server.ts) fixes the orphaned-child-holding-the-port bug (#1690) for CodeZip dev servers, but not for Container agents. On the process.exit(0) path — i.e. Ctrl+C / kill $PID, which is the exact scenario #1695 targets — a Container dev server keeps running and continues to hold the mapped host port.

Root cause

The reaper reaps the child's process group:

// dev-server.ts
process.kill(-pid, 'SIGKILL');

For ContainerDevServer, getSpawnConfig() returns docker/podman/finch run --rm --name <name> -p <hostPort>:<internalPort> ..., so this.child is the run client, not the server. The actual server runs inside the container, owned by dockerd/containerd in a separate process tree. Group-killing the run-client's process group does not stop the container:

  • --rm cleans up on container stop/exit, not on client death — so it never triggers here.
  • SIGKILL to the run client does not propagate to the container for the client/daemon split (true for Docker, Podman/conmon, and Finch/Lima).

Container teardown lives only in the ContainerDevServer.kill() override (spawn(runtimeBinary, ['stop', containerName])), and the process.exit(0) path bypasses kill() entirely — that bypass is the whole reason the reaper exists.

Repro

# On a Container agent (agent with a Dockerfile / --type container)
agentcore dev --logs --port 9137 &
DEV_PID=$!
# wait for the port to bind, then:
kill $DEV_PID          # or Ctrl+C in an interactive session
sleep 4
lsof -ti :9137
# actual:   container still running, port held (reparented; only freed by manual `docker stop`)
# expected: empty — port freed

The next agentcore dev then fails on the held port until the user manually runs docker stop <name>. (Note: the base prepare() rm -f clears a stale name on next launch, but not a still-running container's live port binding.)

Suggested fix

Make the exit reaper container-aware — on the exit path, stop the container by name (mirroring the kill() override's runtimeBinary stop <containerName>) rather than group-killing the run-client pid. Since process.exit's 'exit' handler must be synchronous, a spawnSync(runtimeBinary, ['stop', containerName]) (or rm -f) is likely the right primitive there.

Context

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions