Every recreation commits the container to triple-c-snapshot-{id}:latest and moves that tag; the image it pointed at keeps its layers and loses its name. Nothing deleted those. Measured on a real host: 7 orphans holding 7.4 GB, three of them from a single day's work.
The safety argument
sweep_orphaned_snapshots removes an image only when it is both:
untagged — every image the app depends on carries a tag, so a project's live triple-c-snapshot-{id}:latest and a migration's pre-migration-* rollback pin cannot match the filter at all; and
labelled triple-c.managed=true — which docker commit copies from the container onto the image, making it a reliable mark of provenance. The user's own dangling images are not ours to delete.
Removal is unforced on top of that, so Docker refuses (409) while any container is still built from the image — including the stopped containers of projects that are not running. Those are counted and left for the next sweep. A test pins both filter conditions in place, because losing either one turns a snapshot sweep into a prune of the user's whole image store.
When it runs
After a recreation — the container holding the old image open has just been removed, so this is when Docker will actually let it go.
After a migration is accepted — dropping the rollback pin is precisely what turns the pre-migration snapshot into an orphan. Waiting for that project's next recreation would leave it lying around indefinitely.
Both detached: this is housekeeping, and a full disk is a better outcome than a project that will not start. Each sweep clears every orphan it finds, not just the one it caused, so recreations that predate this change are cleaned up too.
Also: the triple-c.managed label string is now a constant rather than four literals, and the README's Container Lifecycle section documents the behaviour.
Tests
22/22 in docker::container (including the new filter test), 348/348 frontend, cargo check clean with no warnings.
Every recreation commits the container to `triple-c-snapshot-{id}:latest` and moves that tag; the image it pointed at keeps its layers and loses its name. Nothing deleted those. Measured on a real host: **7 orphans holding 7.4 GB**, three of them from a single day's work.
### The safety argument
`sweep_orphaned_snapshots` removes an image only when it is **both**:
- **untagged** — every image the app depends on carries a tag, so a project's live `triple-c-snapshot-{id}:latest` and a migration's `pre-migration-*` rollback pin cannot match the filter at all; and
- **labelled `triple-c.managed=true`** — which `docker commit` copies from the container onto the image, making it a reliable mark of provenance. The user's own dangling images are not ours to delete.
Removal is unforced on top of that, so Docker refuses (409) while any container is still built from the image — including the stopped containers of projects that are not running. Those are counted and left for the next sweep. A test pins both filter conditions in place, because losing either one turns a snapshot sweep into a prune of the user's whole image store.
### When it runs
- **After a recreation** — the container holding the old image open has just been removed, so this is when Docker will actually let it go.
- **After a migration is accepted** — dropping the rollback pin is precisely what turns the pre-migration snapshot into an orphan. Waiting for that project's next recreation would leave it lying around indefinitely.
Both detached: this is housekeeping, and a full disk is a better outcome than a project that will not start. Each sweep clears every orphan it finds, not just the one it caused, so recreations that predate this change are cleaned up too.
Also: the `triple-c.managed` label string is now a constant rather than four literals, and the README's Container Lifecycle section documents the behaviour.
### Tests
22/22 in `docker::container` (including the new filter test), 348/348 frontend, `cargo check` clean with no warnings.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Every recreation commits the container to triple-c-snapshot-{id}:latest
and moves that tag; the image it pointed at keeps its layers and loses
its name. Nothing deleted those, so they accumulate — measured on one
real host, 7 orphans holding 7.4 GB, three of them from a single day's
work.
`sweep_orphaned_snapshots` removes them, under two conditions that are
the whole safety argument. Untagged: every image the app depends on
carries a tag, so a project's live `:latest` and a migration's
`pre-migration-*` rollback pin cannot match the filter at all. And
labelled `triple-c.managed=true`, which `docker commit` copies from the
container onto the image — the user's own dangling images are not ours
to delete. Removal is unforced on top of that, so Docker refuses while
any container is still built from the image, including the stopped
containers of projects that are not running; those are counted and left
for the next sweep.
It runs after a recreation, which is when the orphan it just made
becomes removable, and after a migration is accepted, which is the
moment dropping the pin turns the pre-migration snapshot into an orphan.
Both detached: this is housekeeping, and a full disk beats a project
that will not start. Each sweep clears every orphan it finds, so
recreations that predate it are cleaned up too.
The label string is now a constant rather than four literals, and a test
pins both filter conditions in place.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jknapp
merged commit be37723c38 into main2026-08-12 02:06:24 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Every recreation commits the container to
triple-c-snapshot-{id}:latestand moves that tag; the image it pointed at keeps its layers and loses its name. Nothing deleted those. Measured on a real host: 7 orphans holding 7.4 GB, three of them from a single day's work.The safety argument
sweep_orphaned_snapshotsremoves an image only when it is both:triple-c-snapshot-{id}:latestand a migration'spre-migration-*rollback pin cannot match the filter at all; andtriple-c.managed=true— whichdocker commitcopies from the container onto the image, making it a reliable mark of provenance. The user's own dangling images are not ours to delete.Removal is unforced on top of that, so Docker refuses (409) while any container is still built from the image — including the stopped containers of projects that are not running. Those are counted and left for the next sweep. A test pins both filter conditions in place, because losing either one turns a snapshot sweep into a prune of the user's whole image store.
When it runs
Both detached: this is housekeeping, and a full disk is a better outcome than a project that will not start. Each sweep clears every orphan it finds, not just the one it caused, so recreations that predate this change are cleaned up too.
Also: the
triple-c.managedlabel string is now a constant rather than four literals, and the README's Container Lifecycle section documents the behaviour.Tests
22/22 in
docker::container(including the new filter test), 348/348 frontend,cargo checkclean with no warnings.🤖 Generated with Claude Code
Every recreation commits the container to triple-c-snapshot-{id}:latest and moves that tag; the image it pointed at keeps its layers and loses its name. Nothing deleted those, so they accumulate — measured on one real host, 7 orphans holding 7.4 GB, three of them from a single day's work. `sweep_orphaned_snapshots` removes them, under two conditions that are the whole safety argument. Untagged: every image the app depends on carries a tag, so a project's live `:latest` and a migration's `pre-migration-*` rollback pin cannot match the filter at all. And labelled `triple-c.managed=true`, which `docker commit` copies from the container onto the image — the user's own dangling images are not ours to delete. Removal is unforced on top of that, so Docker refuses while any container is still built from the image, including the stopped containers of projects that are not running; those are counted and left for the next sweep. It runs after a recreation, which is when the orphan it just made becomes removable, and after a migration is accepted, which is the moment dropping the pin turns the pre-migration snapshot into an orphan. Both detached: this is housekeeping, and a full disk beats a project that will not start. Each sweep clears every orphan it finds, so recreations that predate it are cleaned up too. The label string is now a constant rather than four literals, and a test pins both filter conditions in place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>