Relationships
#2652 Docker image runs swamp as PID 1 without an init, so orphaned step processes are never reaped
Opened by hammz · 9/28/2026· Shipped 9/29/2026
Summary
The official swamp image runs swamp as PID 1 with no init, so orphaned child processes are never reaped. Every process a step leaves behind stays a zombie until the container exits.
Dockerfile builds FROM denoland/deno:2.9.7, whose entrypoint is ["/tini", "--", "docker-entrypoint.sh"], and then sets ENTRYPOINT ["swamp"]. That override drops tini. Linux re-parents orphans to PID 1, and Deno only waits on children it spawned itself through Deno.Command, so swamp never collects adopted orphans.
Reproduction
A command/shell step that backgrounds a short-lived process, waits for it to finish, then lists zombies (step.sh):
(sleep 1 &)
sleep 3
echo "PID 1 is: $(tr '\0' ' ' < /proc/1/cmdline)"
for f in /proc/[0-9]*/status; do
grep -q '^State:.*Z' "$f" 2>/dev/null && echo "ZOMBIE: $(awk '/^(Name|Pid|PPid):/ {printf "%s %s ", $1, $2}' "$f")"
doneRun inside denoland/deno:2.9.7 with swamp as PID 1 (the image's layout), and again with /tini as PID 1:
| Container entrypoint | PID 1 | Zombies after the orphan exits |
|---|---|---|
swamp image (ENTRYPOINT ["swamp"]) |
swamp model method run z execute |
sleep, Pid 33, PPid 1 |
base image's /tini |
/tini -- sh /demo/setup.sh |
none |
Impact
- A long-running
swamp servecontainer builds up zombies from every step that leaves a process behind, for as long as it runs, using up PIDs and process-table slots. - After swamp-club#2634, a killed step's process group is signalled as a whole.
shdies without collecting its children, so every descendant below the group leader is handed to swamp and becomes a zombie, unless an intermediate parent handled SIGTERM and waited for its children. The group leader itself is always collected by Deno. - A zombie still counts as a process-group member, so #2634's liveness probe keeps seeing the group as alive. Every cancel inside a container therefore waits the full 3 s grace before its SIGKILL (a no-op on zombies).
- The same applies under Kubernetes, where each container has its own PID namespace.
docker run --initis not available there, so the fix belongs in the image. - PID 1 also gets no default signal dispositions: any signal swamp does not handle is ignored instead of terminating it.
Proposed fix
- Dockerfile:
ENTRYPOINT ["/tini", "--", "swamp"]./tini(v0.19.0) already ships in the base image. It forwardsdocker stop's SIGTERM to swamp, which handles it as today, and it passes swamp's exit code through.docker run <image> serve ...is unchanged. Confirm/tiniis present in the arm64 variant too, sincerelease.ymlbuilds amd64 and arm64. - Pin it: add a check to
integration/toolchain_pins_rules_test.ts, in the same ratchet style as theFROMpin, asserting theENTRYPOINTstarts with/tini, so it cannot be reverted to["swamp"]silently. - Warn: in the
swamp serveandswamp worker connectactions, when running on Linux as PID 1, log a warning that swamp is running as PID 1 without an init and exited children will not be reaped, and suggest--initor tini. This covers users who copy the binary into their own images. The predicate needs a small unit test. - Docs: note in
design/primitives/workflows.md(process-tree termination section) that a container needs an init, and that the image ships one. Also update the manual's swamp-serve page if it describes running the image.
Not proposed: collecting adopted zombies inside swamp through libc waitpid FFI. A blanket waitpid(-1) would steal exit statuses from Deno's own Deno.Command waits. A targeted version would be Linux-specific, and it would still miss orphans from steps that exited normally.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.