2026-06-29 Media Stack Single-Command Deploy
Context: Working on Media Stack IaC Declarative Config Evaluation (branch feature/iac-declarative-config).
What I set out to do
Answer a design question on the media-stack Nix seam: “why can’t the activation script bring up the stack as an idempotent check like signoz does, then apply the remaining items?”
What I actually did
Took it in two steps, each landing a commit.
-
autoStart(2f38b23) — I’d earlier overstated that the host seam can’t start the stack. It can. Added anautoStartoption: after layer-1, run an idempotentdocker compose up -d, guarded ondocker infoso a stopped Docker Desktop skips cleanly instead of failing the switch. Layer-2 deliberately stays with the orchestrator sidecar (the host genuinely can’t reach the unpublished *arr ports, and blocking switch on full-stack health is fragile). -
Vendored deploy dir (
fd61483) — the follow-up question “why must I passprojectDir?” exposed the real fix. The module’s own path is a read-only/nix/storecopy (wrong dir to run compose from), but the deployment location is a fact the module can own — exactly the signoz vendored-compose pattern. Dropped the four path options (configRoot/composeSecretsDir/envFile/projectDir) for onedeployDir(default~/.local/share/media-stack). On switch the module vendors the runtime stack out of the flake source (compose, scripts/, terraform/ modules, static config), seeds layer-1 +.envalongside, and runs compose there. Module is now a factory closed overself. Consumers import the module and never clone the repo.
What was striking
- The good answer came from following the user’s pushback, not my first design.
projectDirwas a smell; “why am I passing this?” → the module should own the location, like signoz already does for them. Consistency with their existing infra was the tell. - rsync discipline matters:
--deletefor scripts/terraform modules, but--excludetfstate so the orchestrator’s state survives a redeploy; config overlay without--deleteand excluding generator-owned paths so the seam stays authoritative. - Verified the deploy by actually assembling the tree in a scratch dir, not just eval — caught nothing broken, but it’s the kind of thing eval won’t tell you.
What was notable / still open
This closes the deploy layer (compose→Nix), so all three layers are now convertible end-to-end in the repo. The one persistent unknown is unchanged: none of it has run against a live home-manager switch — everything is verified in isolation.
Session 2: CI + Nix/Terraform lint hardening
Follow-on the same branch. The repo had prek (pre-commit) hooks locally but CI never ran them — only nix flake check + tofu validate gated PRs, so shellcheck/yamllint/actionlint/codespell could rot.
- Added a CI
pre-commitjob that runsprek run --all-filesinsidenix develop, so the language-agnostic hooks and the new toolchain hooks all run in one job with their tools on PATH. - Added Nix gates:
nixfmt(adopted the RFC-official formatter — one-time ~430-line reformat of all 5 hand-tuned files),deadnix,statix. Plustofu fmtfor Terraform. All as local pre-commit hooks backed by the devShell. - statix surfaced ~6 real nits (useless parens, assign-vs-
inherit, repeated dotted keys). Fixed each at the source — nesting the repeatedhome.*/lib.*keys into single blocks removed the repeated-key warnings cleanly, so nostatix.tomlglobal-disable was needed. - Kept it DRY: pre-commit owns style/lint, the existing
flake check/tofu validatejobs own correctness. No double-checking.just prek+ newjust fmtnow wrapnix developso local == CI.
Notable: the devShell pins nixfmt-1.2.0 (repo nixpkgs) while I’d formatted with 1.3.1 (system channel) — verified they agree by running the hooks through the devShell, the exact path CI takes. All 16 hooks green; nix flake check green.