Disk Management¶
Shed stores cached VM image blobs (the shared lower layers), per-shed writable upper layers, instance metadata, and (on VZ) console logs on each server. This page explains what lives on disk, how to measure it, and how to reclaim space safely.
The three tools involved:
shed system df— read-only disk usage report.shed system prune— scoped cleanup with a dry-run-first UX.- The overlay-in-guest storage model — each shed allocates only a sparse writable upper (default 5 GB), while the read-only lower image is shared across every shed pinning the same digest.
Full flag references live in the CLI reference; full API schemas live in the HTTP API reference. This page focuses on workflows. The on-disk layout itself is covered in Storage Model.
What lives on disk¶
Each server stores four kinds of data in its backend directory:
| Kind | VZ path (macOS) | Firecracker path (Linux) | Created by | Removed by |
|---|---|---|---|---|
| Image blobs | ~/Library/Application Support/shed/vz/blobs/sha256/<digest>/ |
/var/lib/shed/firecracker/images/blobs/sha256/<digest>/ |
shed image build, shed image pull, or shed-server pull-images |
shed image prune (when no shed/snapshot pins the digest) |
| Tags | ~/Library/Application Support/shed/vz/tags/<tag>.json |
/var/lib/shed/firecracker/images/tags/<tag>.json |
Same as above | shed image rm <tag> |
| Kernel / initrd (per blob) | Stored alongside rootfs.ext4 inside each blob directory |
Same | First conversion of an image | Removed with the blob |
| Per-shed writable upper | ~/Library/Application Support/shed/vz/uppers/{name}/upper.ext4 |
/var/lib/shed/firecracker/uppers/{name}/upper.ext4 |
shed create (sparse, default 5 GB; lower image is shared read-only via overlayfs inside the guest) |
shed delete, shed reset, or shed system prune --instances |
| Per-shed metadata | ~/Library/Application Support/shed/vz/instances/{name}/metadata.json |
/var/lib/shed/firecracker/instances/{name}/metadata.json |
shed create |
shed delete or shed system prune --instances |
| VZ console log | ~/Library/Application Support/shed/vz/instances/{name}/console.log |
(Firecracker has none — SDK writes to stderr) | VM boot | shed system prune --logs (truncates to last N bytes) |
| Orphan sidecars | *.tmp from a crashed install staging dir |
Same | Partial or crashed image conversions | shed system prune --orphans |
See Storage Model for the content-addressed layout and Images for how Docker refs resolve to digests via the ref-index.
Measuring usage with shed system df¶
shed system df reports what each server currently holds on disk. The default rollup shows one line per category:
SERVER: prod-mac BACKEND: vz
GENERATED: 2026-04-21T13:36:15Z
CATEGORY FILES LOGICAL PHYSICAL
images 3 20.1 GB 3.3 GB
sheds (0 stopped, 2 run) 5 8.1 GB 712 MB
orphans 1 0 B 0 B
TOTAL 9 28.2 GB 4.0 GB
Two columns matter:
- LOGICAL (
stat.Size) — what tools likedu -k --apparent-sizesee. For a 5 GB sparse upper layer, this is 5 GB regardless of how much data has actually been written into it. - PHYSICAL (
stat.Blocks * 512) — how much the filesystem reports as allocated. With the overlay-in-guest model, per-shed rows now report only the per-shed writable upper (typically hundreds of MB), and the read-only lower image is reported once underimagesrather than under every shed pinning it.
Add -v for per-image and per-shed rows, --json for machine-readable output, and --all to fan out across every configured server:
shed system df
shed system df -v
shed system df --json | jq '.totals'
shed system df --all # Every configured server
shed system df -s mini2 # Specific server
The full flag table is in the CLI reference. The raw response schema is documented under GET /api/system/df.
Reclaiming space with shed system prune¶
shed system prune runs a scoped cleanup pass with four scopes that can be combined:
--images— remove cached images that aren't referenced by config (default_image/image_aliases) or any existing shed.--instances— delete stopped sheds older than--until(default 72 h).--logs— truncate VZ console logs to the last--log-tail-bytes(default 5 MiB). No-op on Firecracker.--orphans— remove leftover state from crashed operations:*.tmpstaging directories from interrupted image installs, partial snapshot directories whosesnapshot.jsonnever landed, and per-sheduppers/<name>/directories whosemetadata.jsonwas never written. Lock files are preserved to avoid an inode-reuse race, and any.creatingmarker fresher than 1 h keeps its directory in skipped status so in-flight operations aren't swept.
When no scope flags are set, the command applies the default scope: --images --instances --orphans (not --logs, which is always opt-in).
The command is always dry-run-first. It prints the candidate table, then prompts for confirmation unless --force is set:
$ shed system prune
SERVER: prod-mac (dry-run) --until 72h0m0s scope=images+instances+orphans
IMAGES (2, 40.0 GB)
NAME PATH LOGICAL PHYSICAL
ghcr.io/charliek/shed-vz-base:v0.6.0 /Users/alice/Library/Application Support/shed/vz/blobs/sha256/abc123.../rootfs.erofs 20.0 GB 1.9 GB
ghcr.io/charliek/shed-vz-full:v0.6.0 /Users/alice/Library/Application Support/shed/vz/blobs/sha256/def456.../rootfs.erofs 20.0 GB 3.2 GB
SKIPPED (3)
KIND NAME/PATH REASON
instance api-dev cannot prune running shed
instance api-test too recent (3h < 72h)
lock /Users/alice/Library/Application Support/shed/vz/blobs/sha256/abc123.../rootfs.ext4.lock lock file retained (inode-reuse race safety)
TOTAL TO FREE: 40.0 GB logical / 5.1 GB physical (2 items)
Proceed? [y/N]
The 72 h age gate filters by mtime(metadata.json), which is refreshed on every state change. --until 0s is an explicit "any age" escape hatch that still skips running sheds. --all fans out across every configured server; --json --force is required to execute non-interactively, and --json --dry-run is always allowed.
Full flag table, scope semantics, and the internal deletion ordering are in the CLI reference. The raw request/response schema is under POST /api/system/prune.
Physical bytes vs. reclaimed disk¶
The PHYSICAL freed total is attributed per file from stat.Blocks * 512. When the file being removed shares extents with another file (clonefile, FICLONE, or hardlinks), the bytes the filesystem actually reclaims may be lower than what the report attributes. Compare shed system df before and after to measure true reclamation.
Per-shed cost: writable upper layer¶
shed create allocates a per-shed sparse upper file under uppers/{name}/upper.ext4 (5 GB by default; configurable via --upper-size <N>G or the server's upper_size_default). The read-only lower (the cached image blob) is shared across every shed pinning the same digest, so multiple sheds on the same image share both disk and the host page cache.
The shed's in-guest initramfs mkfs.ext4-formats the upper on first boot, then mounts it as the writable layer of an overlayfs stack on top of the lower. Writes inside the VM grow the upper; reads from the lower are zero-cost beyond the host's first page-cache miss.
Per-shed disk cost stays small (typically a few hundred MB to a couple GB after a busy session), independent of host filesystem reflink support — the cost is borne inside the guest's overlay rather than via reflink between two host files.
Workflows¶
"Where did 40 GB go?"¶
The verbose view shows per-image and per-shed rows so you can identify the largest consumers. The shared lower image is reported once under images, not duplicated under every shed pinning it.
Clean up before a deploy¶
Default scope covers unreferenced images, stopped sheds older than 72 h, and orphan sidecars. Running sheds, recently-stopped sheds, and live conversion locks are all skipped with an explicit reason.
Trim runaway console logs¶
VZ console.log files grow unbounded during long-running sheds. This keeps the last 1 MiB of each log and discards the rest. The truncation happens in place so vfkit keeps writing past the new EOF; a tiny window of writes can be lost between the read-tail and truncate. On Firecracker this is a no-op (no per-instance console log exists).
Fleet-wide cleanup¶
Fan-out is client-side: each configured server is queried in parallel. Offline or older-version servers (missing the /api/system/prune route) are reported inline and the command still exits 0, so partial fleet upgrades don't block cleanup.