Skip to main content
Version: 1.4

Reclaiming host resources

Fadebox owns Docker resources by labelling them, not by keeping a ledger. That is what makes the daemon the source of truth for what is running — but it leaves nobody responsible for what is no longer claimed. Named volumes outlive the instances that wrote them — stopping or deleting an instance leaves them, and only Reset removes an instance's own — a restored or reset database knows nothing about what an earlier one deployed, and a host that was disconnected while instances ran keeps them forever.

The Resources on host panel is the answer to "what is fadebox still holding here, and what of it is nobody's?" It reports before it removes, and every removal is an explicit act. The same panel answers the other half of a full disk — the images pulled onto the host and never removed — with the same posture.

Where to find it​

Administration → Runtimes → the runtime → Resources on host (global admin only). The panel shows a count — 12 managed, 3 orphaned — and lists each orphan as kind, name and who it used to belong to:

CONTAINER shop-dev-pr42-web — instance 7 is gone
NETWORK shop-dev-pr42 — instance 7 is gone
VOLUME shop-dev-pr42-db-data — instance 7 is gone

The scan is read-only and runs every time you open the panel. Nothing is removed until you press a button.

The same panel carries Refresh instances, which re-reads every running instance on this runtime from the daemon and updates its health — the on-demand version of the sweep fadebox runs every five minutes by default as the safety net behind the daemon's event stream. It removes nothing; use it when you want a card's health caught up now rather than at the next tick.

None of this exists on an elastic runtime. The scan, the reclaim, the purge and the image panel all read a runtime's own daemon, and such a runtime has none — its hosts are the machines under it, and each of those is one instance's and is destroyed whole rather than tidied. Every one of those calls answers 409 saying so, and the image retention timer never reaches such a runtime either. Refresh instances is the exception: health is read off each machine, so re-checking a whole elastic runtime works exactly as it does here. What accumulates on such a runtime is machines, and Keeping track of machines is the panel for that.

What counts as an orphan​

Every resource fadebox creates carries fadebox.managed, plus a label saying whose it is. The scan reads that label back and asks whether the owner still exists:

The resource carriesOwner shown asOrphaned when
An instance idinstance 7No instance row with that id exists, or the instance has been moved to another runtime — its volumes stay on the daemon it left, and this is the only place they are ever freed
A template test-run nametemplate test 'postgres'No template with that slug exists (a test run is never persisted, so the template's existence is what makes its containers legitimate)
NeitherunknownAlways — it cannot be attributed to anything

Two things are deliberately outside this:

  • The ingress stack. Traefik and its socket proxy carry fadebox.system, not fadebox.managed, so they can never be reported as orphans or caught by a purge. They have their own install and remove actions — see Ingress.
  • Anything created before fadebox labelled it. It is invisible here and has to be cleared by hand with docker.

Reclaim: remove the orphans​

Reclaiming removes only resources whose owner is gone, so it is safe to run against a runtime with live instances on it. It is split in two, because the kinds are not equally recoverable:

  • Containers and networks are rebuilt by a redeploy. This is the default button.
  • Volumes hold data — a database, uploads, whatever the containers wrote — and the instance being gone does not make the data worthless. They are a separate button behind a confirmation, and there is no undo.

Removal runs containers first, then networks, then volumes: a network with an attached container, or a volume with a live mount, is refused by the daemon, so any other order leaves the rest behind.

The result counts what the daemon actually removed. A refusal is a warning in the server log, not a failed request — so if a volume was still mounted, it simply is not in the count, and the next scan still lists it.

Purge: remove everything​

Purge ignores ownership and removes every fadebox.managed resource on the host, including those belonging to live instances, including every volume.

It exists for one situation: the host's database is gone. That is exactly when reclaim cannot help — a fresh database reissues row ids that stale containers still carry, so the ownership diff reports leftovers as live and healthy. Purge skips the diff entirely.

Anywhere else, reclaim the orphans instead. The confirmation dialog says how many live resources you are about to take with you.

Images: the other half of the disk​

Everything above frees what Fadebox created. Images are not created, they are pulled, and until now nothing in Fadebox removed one: a per-PR tag, last month's nightly :dev, the image of an environment since deleted — they stay on the host forever, and on most runtimes they are the largest thing on the disk.

docker image prune -a is the blunt version of this and removes the wrong things: the image a stopped instance would redeploy from, and everything an air-gapped host can never pull again. Fadebox classifies instead, from the daemon's own image listing plus its deploy records.

Four states​

StateWhat it meansWho may remove it
In useA container references it, running or stopped.Nobody — the daemon refuses anyway.
WantedNo container, but the latest deploy record of an instance that still exists names it, or it is one of Fadebox's own pinned system images (the ingress stack).Nobody. This is the cache that makes a redeploy fast and an air-gapped runtime possible.
UnusedNothing wants it, and Fadebox knows the image: some retained deploy record or a template's literal image: names it, or it is the untagged leftover of a tag that moved.An admin, for the runtime; a deployer, for what only their project pulled; the retention timer.
ForeignNothing on this installation has ever named it — an operator's own images on a shared daemon, another installation's, a colleague's docker pull.An admin, and only by asking for it explicitly.

The first state that applies wins, so an image with two tags is one image and takes the strongest state any of them earns. Matching folds the spellings a pull folds: postgres:16, docker.io/library/postgres:16 and an implicit :latest are one reference. An untagged image is Fadebox's only when nothing points at it at all, or when a digest does and this installation pulls from that repository — a stranger's docker pull nginx@sha256:… on a shared daemon is foreign, and stays behind the confirmation.

Every byte figure is unique bytes: each image's size less the layers a neighbour also holds. What a panel offers is at least this much comes back, rather than a total that counts one shared base layer once per image sitting on it.

The runtime door: an admin, host-wide​

The Resources on host panel carries an Images section — the four states with their counts, the two reclaimable ones with their bytes, and the unused and foreign images listed with tags, size, age and, for unused ones, which projects pulled them. The read walks the host live, as the orphan scan does.

Remove unused images removes the unused state, dangling leftovers included, and reports what actually went. Foreign images are listed and never touched unless you tick Also remove images fadebox did not pull, which asks for confirmation the way the volumes button does.

Removal is never forced, so the daemon's own refusals are the last safety check: an image a container still holds, or one a second tag names, stays and is simply not in the count.

The project door: a deployer, for their own​

Clearing up after your own runs is running, not administering, so a deployer does not need an admin for it. The project's Environments page carries a line — Images left behind by this project: 3.2 GiB on local — with Remove beside it for anyone holding deployer. A viewer sees the figure without the button, and nobody sees a line when there is nothing.

It offers strictly less than the admin's button: an unused image is the project's only when no other project's instances ever pulled it. The postgres:16 everyone runs is not theirs to remove, and is not offered. The worst case is one of the project's own instances pulling again on its next deploy.

The figure comes from the last disk pass rather than a live walk, because this page is opened constantly — so it can be up to a quarter of an hour old. The removal itself re-reads the host before it removes anything and writes the new state back, so the line updates as soon as you press it.

Retention: remove them on a timer​

Every runtime has an image retention setting: keep unused images for this many days, then remove them automatically. It is unset by default, which means never. The disk pass applies it, on each image's last mention — the newest retained deploy record naming it, or the image's own creation time when no record does.

The timer removes exactly what the everyday button removes: never a wanted image, never a foreign one. It is off by default because only the operator can weigh the trade — a laptop wants it on and short, an air-gapped runtime wants it off forever, and a surprise re-pull on Monday morning is the kind of implicit action Fadebox does not take on its own. Once it is on, you have said what you want.

Over the API the field follows the same three-way contract as the ingress port: null leaves it unchanged, 0 switches the timer off, ≥ 1 sets the days.

Nothing younger than five minutes​

An image a deploy pulled a moment ago, before its container exists, is unused by every rule above and would vanish between the pull and the create. Both doors and the timer therefore skip images younger than FADEBOX_RUNTIME_IMAGE_MINIMUM_AGE (five minutes): a reclaim cannot race a deploy.

What gets recorded​

Both actions write an audit event on the runtime — runtime.reclaimed or runtime.purged — with the per-kind counts and the names of what was removed. The name list records only what the daemon confirmed: a refused deletion is never written down as a deletion. See Audit log.

Every image removal writes runtime.images_reclaimed on the runtime, whichever door it came through: the count and the bytes freed, the project when it was the project's button, and foreign: true when an admin asked for those too. The retention timer records the same event as the system actor, so an automatic removal is as readable afterwards as a pressed one.

Over the API​

The three orphan calls are admin-only.

CallDoes
GET /api/runtimes/{name}/resourcesThe scan. Returns kind, name, owner, orphaned per resource. Read-only.
POST /api/runtimes/{name}/resources/reclaimRemoves orphans of the granted kinds.
DELETE /api/runtimes/{name}/resourcesPurges everything managed, owned or not.

The image calls follow the two doors:

CallRoleDoes
GET /api/runtimes/{name}/imagesadminThe live classification: every image with its state, tags, sizes, age, last mention and the projects that pulled it, plus unusedBytes and foreignBytes.
POST /api/runtimes/{name}/images/reclaimadminRemoves the unused images; {"includeForeign": true} takes the foreign ones too. Answers with images and bytes.
GET /api/projects/{project}/imagesviewerPer runtime, the unused images only this project ever pulled, and their bytes.
POST /api/projects/{project}/images/reclaimdeployerRemoves them. Answers with images and bytes.

Retention rides on the runtime itself, as imageRetentionDays on GET/POST/PUT /api/runtimes — null unchanged, 0 off, ≥ 1 days.

The reclaim body's kinds is a permission, not a filter, and the distinction matters on a destructive endpoint:

# containers and networks — the recoverable default
curl -sf -X POST -H "$AUTH" "$HOST/api/runtimes/local/resources/reclaim"

# volumes too: data goes with them, so it must be asked for by name
curl -sf -X POST -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"kinds":["CONTAINER","NETWORK","VOLUME"]}' \
"$HOST/api/runtimes/local/resources/reclaim"

# an explicitly empty list grants nothing and removes nothing
curl -sf -X POST -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"kinds":[]}' "$HOST/api/runtimes/local/resources/reclaim"

An absent kinds (or no body at all) falls back to the recoverable default — containers and networks. An empty one grants nothing, which is what a caller building the request from a multi-select with everything deselected means by it.

Both removal calls answer with what was removed, by kind:

{ "containers": 2, "networks": 1, "volumes": 0 }

There is no total; it is the sum of the three, and a client that wants one adds them up.