Give each CI run its own compose project so runs stop deleting each other #43

Merged
geusebio merged 1 commit from ci/unique-compose-project-name into main 2026-08-08 02:07:52 +00:00
Owner

./test runs docker compose -f test.yml with no project name, so compose derives one from the working directory — docker, identically for every branch and every run, on a daemon they all share.

Two consequences

Container names collide. From run 299:

Conflict. The container name "/docker-php-83-assertions-1" is already in use
by container "6fd9c9fd8e8e..."

And runs delete each other. test's EXIT trap is:

$DC_CMD --progress quiet down --timeout 0 --remove-orphans --volumes

against that shared project. Any run that finishes — or fails early — removes the containers of every other run still in flight. The victim dies mid-build having emitted no error of its own.

Run 291 is precisely that: 4863 lines of healthy build output, then the log simply stops, then a failed job with no error marker anywhere in it. That signature is indistinguishable from the disk-pressure and buildkit-GC contention CLAUDE.md describes, and it has been read as that more than once.

This has never reproduced locally because anyone working from a git worktree already gets a unique project name for free, from the worktree's directory.

The change

COMPOSE_PROJECT_NAME=docker-<run id>, falling back to the pid off CI, with an explicit value from the caller still winning.

Isolation has a price, and reap-buildkit-orphans pays it: a hard-killed run can no longer clean up after itself, and unlike the old shared project, nothing else will ever adopt what it leaves. So the reaper now also collects abandoned test projects.

Selection is deliberately conservative, because this daemon is a live Swarm manager:

  • on compose's own project label, not a name substring — --filter name=docker- would match half the daemon
  • restricted to the docker-<digits> shape ./test generates
  • never the project doing the reaping
  • reusing the existing 12h cutoff, so a run in flight is never a candidate

Testing

  • ./test under a unique project name: passes, and leaves nothing behind.
  • Project name verified in all three cases — CI (docker-12345), local (docker-<pid>), and caller override.
  • The reaper's selection was dry-run against this workstation's daemon with the cutoff widened to now — the most aggressive case possible — across 30 containers in 7 compose projects. It selected the single planted abandoned container and nothing else. The 7 real projects (printshop, someones-edge, surgeon, sw, …) were all spared.

Why this is worth landing before anything else

Four sessions are currently pushing to this repo, each triggering a full-matrix build. Runs 297, 298 and 299 all failed inside that window; 300 passed once it thinned out. Until this lands, every run here is exposed to being torn down by an unrelated one, and every resulting failure looks like a bug in whatever change happened to be under test.

`./test` runs `docker compose -f test.yml` with no project name, so compose derives one from the working directory — **`docker`, identically for every branch and every run**, on a daemon they all share. ## Two consequences **Container names collide.** From run 299: ``` Conflict. The container name "/docker-php-83-assertions-1" is already in use by container "6fd9c9fd8e8e..." ``` **And runs delete each other.** `test`'s EXIT trap is: ```bash $DC_CMD --progress quiet down --timeout 0 --remove-orphans --volumes ``` against that shared project. Any run that finishes — or fails early — removes the containers of every other run still in flight. The victim dies mid-build having emitted no error of its own. Run 291 is precisely that: 4863 lines of healthy build output, then the log simply stops, then a failed job with no error marker anywhere in it. That signature is indistinguishable from the disk-pressure and buildkit-GC contention CLAUDE.md describes, and it has been read as that more than once. This has never reproduced locally because anyone working from a git worktree already gets a unique project name for free, from the worktree's directory. ## The change `COMPOSE_PROJECT_NAME=docker-<run id>`, falling back to the pid off CI, with an explicit value from the caller still winning. Isolation has a price, and `reap-buildkit-orphans` pays it: a hard-killed run can no longer clean up after itself, and unlike the old shared project, nothing else will ever adopt what it leaves. So the reaper now also collects abandoned test projects. Selection is deliberately conservative, because this daemon is a live Swarm manager: - on compose's **own project label**, not a name substring — `--filter name=docker-` would match half the daemon - restricted to the `docker-<digits>` shape `./test` generates - never the project doing the reaping - reusing the existing 12h cutoff, so a run in flight is never a candidate ## Testing - `./test` under a unique project name: passes, and leaves nothing behind. - Project name verified in all three cases — CI (`docker-12345`), local (`docker-<pid>`), and caller override. - The reaper's selection was dry-run against this workstation's daemon with the cutoff widened to `now` — the most aggressive case possible — across **30 containers in 7 compose projects**. It selected the single planted abandoned container and nothing else. The 7 real projects (`printshop`, `someones-edge`, `surgeon`, `sw`, …) were all spared. ## Why this is worth landing before anything else Four sessions are currently pushing to this repo, each triggering a full-matrix build. Runs 297, 298 and 299 all failed inside that window; 300 passed once it thinned out. Until this lands, every run here is exposed to being torn down by an unrelated one, and every resulting failure looks like a bug in whatever change happened to be under test.
Give each CI run its own compose project so runs stop deleting each other
All checks were successful
Build / Run container tests (push) Successful in 25m8s
Build / Build (push) Successful in 42m23s
5e0a494a48
./test ran `docker compose -f test.yml` with no project name, so compose
derived one from the working directory: "docker", identically for every
branch and every run, on a daemon all of them share. Two consequences,
the second much worse than the first.

Container names collide. From run 299:

  Conflict. The container name "/docker-php-83-assertions-1" is already
  in use by container "6fd9c9fd8e8e..."

And the EXIT trap runs `down --remove-orphans --volumes` against that
shared project -- so any run that finishes, or fails early, deletes the
containers of every other run still in flight. The victim dies mid-build
having emitted no error of its own. Run 291 is exactly that: 4863 lines
of healthy build output, then nothing, then a failed job. That signature
is indistinguishable from the disk and buildkit-GC contention CLAUDE.md
describes, and has been read as it more than once.

This never reproduced locally because anyone working from a git worktree
already had a unique project name, courtesy of the worktree's directory.

COMPOSE_PROJECT_NAME is now docker-<run id>, falling back to the pid off
CI, and an explicit value from the caller still wins.

Isolation costs something, and reap-buildkit-orphans pays it: a killed run
can no longer clean up after itself, and unlike the old shared project
nothing else will ever adopt what it left. So the reaper now also collects
abandoned test projects. It selects on compose's own project label rather
than a name substring -- `--filter name=docker-` would match half the
daemon -- restricted to the docker-<digits> shape this script generates,
never the project doing the reaping, and reusing the same age cutoff so a
run in flight is never a candidate. Verified against this workstation's
daemon: with the cutoff widened to "now", across 30 containers in 7
compose projects, it selects the one planted abandoned container and
nothing else.
Author
Owner

Further evidence from the runs that have gone through since this was opened. The shared project name is now confirmed to break runs in two distinct ways, not one.

Run 302 — the name collision:

service:base-alpine-22-assertions:1 Error response from daemon: Conflict.
The container name "/docker-base-alpine-22-assertions-1" is already in use
by container "f0dd7e3b2e07..."

Run 301 — the shared network deleted mid-run, which I had not seen before:

Error response from daemon: failed to set up container networking:
Could not attach to network docker_default: rpc error: code = NotFound
desc = network docker_default not found

docker_default is the default network of the shared docker project. A sibling run's down --remove-orphans --volumes removed it while run 301 was still attaching containers to it. Same root cause as the conflict, opposite symptom, and this one produces an error that points nowhere near the real problem.

So the tally on this daemon is now:

Run Symptom Cause
291 log stops mid-build, job fails, no error marker at all containers removed by a sibling's cleanup
299 container name ... already in use name collision
301 network docker_default not found shared network removed mid-run
302 container name ... already in use name collision

Worth noting how the recent record reads without this context: of runs 295–302, the only two that passed (295 and 300) were both No image inputs changed; nothing to test — they built nothing. Every run in that window that actually built something failed, and each failure looked like a defect in whatever change happened to be under test.

One useful property while this rolls out: the fix is unilaterally protective. A run on this branch is already immune to a sibling on main tearing it down, because the sibling's down targets project docker and this one lives in docker-<run id>. It does not need every branch to adopt it before it starts helping.

Further evidence from the runs that have gone through since this was opened. The shared project name is now confirmed to break runs in **two** distinct ways, not one. **Run 302** — the name collision: ``` service:base-alpine-22-assertions:1 Error response from daemon: Conflict. The container name "/docker-base-alpine-22-assertions-1" is already in use by container "f0dd7e3b2e07..." ``` **Run 301** — the shared *network* deleted mid-run, which I had not seen before: ``` Error response from daemon: failed to set up container networking: Could not attach to network docker_default: rpc error: code = NotFound desc = network docker_default not found ``` `docker_default` is the default network of the shared `docker` project. A sibling run's `down --remove-orphans --volumes` removed it while run 301 was still attaching containers to it. Same root cause as the conflict, opposite symptom, and this one produces an error that points nowhere near the real problem. So the tally on this daemon is now: | Run | Symptom | Cause | |---|---|---| | 291 | log stops mid-build, job fails, **no error marker at all** | containers removed by a sibling's cleanup | | 299 | `container name ... already in use` | name collision | | 301 | `network docker_default not found` | shared network removed mid-run | | 302 | `container name ... already in use` | name collision | Worth noting how the recent record reads without this context: of runs 295–302, the only two that passed (295 and 300) were both `No image inputs changed; nothing to test` — they built nothing. **Every run in that window that actually built something failed**, and each failure looked like a defect in whatever change happened to be under test. One useful property while this rolls out: the fix is unilaterally protective. A run on this branch is already immune to a sibling on `main` tearing it down, because the sibling's `down` targets project `docker` and this one lives in `docker-<run id>`. It does not need every branch to adopt it before it starts helping.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Grey.ooo/docker!43
No description provided.