2026-06-27 Storage Cleanup ~208G Reclaimed
What I set out to do
Storage volumes were nearly full: a single APFS container at 97% with only 71G free on a 1.9T disk. Goal was to find and reclaim space.
What I actually did
Surveyed top consumers and worked through them. Reclaimed ~208G total (71G → 279G free, 97% → 85%):
- Tool caches (~5.8G): playwright, pip, go-build, java-db — all regenerable.
- Docker prune + raw auto-trim (~20G):
docker system prune -a --volumes; modern Docker Desktop auto-trimmedDocker.raw63G → 43G. - Media-stack duplicate downloads (~89G): removed 5 stopped One Piece torrents (episodes 1086–1143) via the qBittorrent API (
POST /api/v2/torrents/deletedeleteFiles=true), after confirming all 58 episodes were already imported into the tv library. - Nix store GC (~26G, 78G → 52G): only worked after granting the terminal App Management permission (see below).
- FLUX models (~55G): dropped
flux1-schnell,flux1-dev, andt5xxl_fp16from ComfyUI — re-downloadable from HuggingFace. Kept the SDXL checkpoint and loras. - Project
.venvs (~2.4G): removed 18 stale virtualenvs under~/projects, recreatable withuv.
What was striking
Two non-obvious diagnoses:
-
The media-stack silently duplicates every import. A downloads file and its tv counterpart had distinct inodes, link count 1, identical size — proof of a full copy, not a hardlink. Cause: Sonarr/Radarr mount
/downloadsand/tvas separate bind mounts, so hardlinks can’t span them and the *arr falls back to copying. Compounded by qBittorrent’smax_ratio_act=0(pause), which means torrents complete, pause, and are never removed — so~/media/downloadsrefills indefinitely. The real fix is a single/dataparent mount (TRaSH guides) plus a seeding policy that removes. -
nix-collect-garbageaborted freeing 0 bytes on macOS because a dead store path held a.appbundle and App Management TCC blockschmodon it (Operation not permitted). Granting Ghostty App Management + Full Disk Access and restarting it unblocked the GC. Same protection class as the Zotero copyApps issue. The generation-trim half still worked despite the store-GC abort.
Deferred
- Structural single-
/dataremount for the media-stack so imports hardlink instead of copy. - qBittorrent seeding policy fix (
max_ratio_act→ remove, or Sonarr “Remove Completed Downloads”).
Related
Media Stack Download Pipeline Recovery · captured findings to project memory (media-stack hardlink duplication, nix GC App Management block).
Design session: Media Stack IaC evaluation
Separate session, same day. Picked up the feature-opus-4-7-issue-32 branch (the Terraform-for-layer-2 PoC + RFC), rebased it onto origin/main, and used it as the springboard for a longer architecture discussion about declarative IaC for the whole media-stack. Tracked in the new project note Media Stack IaC Declarative Config Evaluation.
What unfolded
Started by reviewing the existing terraform-eval.md RFC (layer 2: partial migration to devopsarr providers viable; Bazarr/qBittorrent have no provider; recommendation is observe-then-decide). Then worked outward to layer 1 (currently mstack + Jinja) and asked how much a declarative system (NixOS / home-manager) actually solves there.
The thread that made it click:
- Secrets reframing. Treating API keys as ephemeral, machine-minted, network-protected tokens (not durable artifacts to guard at rest) means secret generation can leave layer 1 entirely. That removes the impurity that made the static layer awkward for any hermetic tool. The cleanest form is letting each app self-mint its key on boot and reading it back, or Terraform’s
random_*+ provider-apply. - Prior-art survey. Deployment + static config is solved by Nix (arion, oci-containers, compose2nix). App-config is not: every project that tried to own it in a Nix module (declarative-arr, nixarr’s settings-sync) reduces to API reconciliation and is abandoned or flagged unstable. declarative-arr’s own README: “they all work via sending api requests to mimic a user configuring them,” and it explicitly can’t do custom formats.
- The payoff. This stack is already ahead of that ecosystem because Recyclarr owns quality profiles / custom formats — the exact thing the Nix modules can’t. So the right architecture is delegating: Nix deploys + generates static, Recyclarr owns quality, a thin scoped Terraform/
curlreconciler owns the rest. Not one module absorbing everything.
Wrote it up as docs/rfcs/nix-orchestration-eval.md (committed 07b0e27, emdashes scrubbed per house style). Closed the stale PoC PR #33 and renamed the branch to feature/iac-declarative-config.
What was striking
The Infrastructure as Code note I’d already written predicted the whole conclusion: layer first, one writer per object, never bake mutable app state into a Nix closure. The *arr apps storing config in app-owned SQLite is exactly the “mutable app state” anti-pattern, which is why file-based declarative config of them is impossible and everyone falls back to API reconciliation. The mutable-file ownership conflict that would otherwise block home-manager is already solved here by the mkComposeDeploy activation pattern (Homelab Services Architecture).
Open threads
- The Terraform Sonarr PoC still needs ~1 week of observation before the migrate-vs-keep call.
- Layer-1 decision (slimmed
mstackvs home-manager generation) hinges on whether the repo must stay host-agnostic for other people.
Related
Media Stack IaC Declarative Config Evaluation · Infrastructure as Code · Terraform · Homelab Services Architecture
Update: built the Nix Sonarr PoC
Went from analysis to a working artifact. The Nix evaluation was lopsided (Terraform shipped a PoC with parity evidence; Nix was prose only), so I built the parallel PoC: a shared pure config.xml renderer feeding a runnable home-manager backend and a NixOS sketch (the backend seam). The activation seam (generate → mint-or-read secret → seed writable → host facts → tofu apply against terraform/sonarr) evaluates and builds; a flake check proves the Nix render matches the mstack template byte-for-byte. Committed f29d87d on feature/iac-declarative-config.
The satisfying part: the “can I build a NixOS module and run it on my Mac?” question resolved cleanly. No - NixOS modules compile to systemd, which macOS can’t run - but the macOS-native equivalent (a home-manager module) gives the same declarative-module authoring with a Docker-on-mac backend, and a shared renderer keeps a future NixOS host one thin module away. Tracked in Media Stack IaC Declarative Config Evaluation.
Update: converted the entire mstack config generation to Nix
Pushed the Nix PoC further: ported all 8 mstack templates to pure Nix renderers and proved byte-for-byte parity against the real mstack Jinja Environment (a Python oracle that replicates StrictUndefined/autoescape=False/default trailing-newline). config-parity check: 9/9 identical (nginx rendered both auth-off and auth-on).
Two Jinja gotchas the oracle earned its keep on: keep_trailing_newline defaults False (outputs have no trailing newline - my first Sonarr check was diffing the raw .j2 and was quietly wrong), and the {% if %} blocks emit predictable blank lines. The sharper finding: byte parity requires text-templating, not pkgs.formats - the templates are hand-authored text with holes, so pkgs.formats would reformat them. That corrects the RFC’s “4 of 8 native” optimism.
Annoying tail: adding Python files tripped the project-wide ty hook, surfacing a pre-existing environment.py type error (fixed separately), and the hook’s uv run keeps rewriting uv.lock with an env exclude-newer - the mystery stray uv.lock change from the very start of today. And .gitignore’s Python lib/ rule was silently swallowing nix/lib/, so the renderers nearly didn’t commit. Caught it via git ls-files. Commits 89a4eb1 + ae66eff on Media Stack IaC Declarative Config Evaluation.