Table of contents
Wrapper-Repository Packaging
- Type: Runbook
- Status: Current
- Scope: Packaging an upstream service in an independently maintained flake and consuming it from
nixos-config- Canonical sources:
AGENTS.md;flake.nix;lib/auto-update-policy.nix;scripts/attic-cache-build.sh;modules/services/redview.nix;modules/services/navidrome-mcp.nix;modules/services/forgejo-mcp.nix;.sops.yaml;justfiles/deploy.just;justfiles/secrets.just; ADR-002- Last verified: Source commit
556df88494686003b1c4f20c8e0b99b5afc16a6e(2026-09-12); source HEADb2831d33132967b4c4fb69e6068b2b05fff8fdffchanges onlyflake.lock. Repository contracts checked 2026-09-13; no package was built or deployed for this page.- Review triggers: New or removed flake inputs; auto-update policy or controller changes; package/module output changes; SOPS, network, service, CI, or cache contracts
Purpose
Use this procedure when an application is worth operating as a native NixOS
service but lacks a suitable package, or its reproducible build and release
cycle deserve ownership outside the fleet configuration. ADR-002
owns the placement decision. A wrapper repository owns its upstream pin,
package, build checks, and (when reusable) NixOS module. nixos-config owns
the pinned input, host integration, secrets, access, state, backup intent,
monitoring, and deployment.
Redview is the current full example: its independent flake exports a package,
NVIDIA closure, and NixOS module; modules/services/redview.nix imports the
module and integrates Lyra's NFS media, secrets, and backup. Navidrome MCP is
a smaller independent package consumed by modules/services/navidrome-mcp.nix.
The earlier Librarry trial established the pattern but was retired because the
application did not fit the fleet. These examples do not imply that every
missing nixpkgs package needs a wrapper repository.
Prerequisites and safety boundary
- Open an issue for the service, with an owner for upstream updates and build
failures, an operational-fit assessment, affected hosts, runtime data,
clients, and a retirement path. Use a branch and merge request for the
cross-repository/service change under
AGENTS.md. - Check first for a usable nixpkgs package or upstream NixOS module. Read the upstream source, build files, Dockerfile if present, runtime entrypoint, migrations, static assets, required native libraries, and proxy rules. Prove that the software can be built and tested reproducibly before adding a root flake input. A successful package build alone does not prove that the service is safe or useful to operate.
- Identify authoritative state and schema migration behavior before exposing the new service to real clients. Packaging and cutover are separate changes: follow Service Migration and Cutover when an existing instance or data is involved.
- Do not put runtime passwords, tokens, or plaintext environment files in the wrapper flake, Nix store, Git, process arguments, or build logs. SOPS files and runtime credential delivery belong at the fleet integration boundary.
Ordered procedure
1. Establish the wrapper contract
Create the dedicated repository only after the buildability review. Pin the
upstream source in its flake.lock (or an equivalently immutable source),
define clearly named package outputs, and add a NixOS module only when it is
reusable outside one host's integration. Keep the derivation and tests in that
repository, not under hosts/ in nixos-config. Use nixpkgs.follows in the
consumer only when its dependency contract permits it; Redview does not set
one, while Navidrome MCP does.
In the wrapper checkout, first inspect output names, then build the exact package or check that the consuming host will use:
nix flake show
nix build .#<package-output> --no-link
nix flake check
Replace <package-output> with a real output. For fixed-output dependency
hashes, use the builder's reported mismatch to calculate the correct value,
then rebuild; do not accept a placeholder hash or an unbuilt CI workflow.
Test the installed executable, assets, migrations, and runtime closure rather
than treating derivation success as an end-to-end service test. If the wrapper
publishes cache artefacts, verify its own build/push workflow separately; the
fleet's Attic workflow does not automatically prove every wrapper output was
prewarmed.
2. Add the pinned consumer input and update policy
Add the wrapper to flake.nix and consume its actual output from the relevant
module. Existing patterns are inputs.redview.nixosModules.default plus
inputs.redview.packages.${pkgs.stdenv.hostPlatform.system}.redview-nvidia,
and inputs.navidrome-mcp.packages.${pkgs.stdenv.hostPlatform.system}.default.
makeNixosSystem already passes inputs through specialArgs; no second
global plumbing layer is needed.
In the same source-repository change, classify the new root input in
lib/auto-update-policy.nix: one group per updating input with the real
consumer hosts, or an explicit pinned entry and rationale. Do not copy the
legacy instruction to edit scripts/nixos-update-common.sh; that file is
provided by the separately pinned nixos-auto-update package, which
scripts/attic-cache-build.sh builds and sources. The repository's update
regression check compares the policy with the actual root inputs and host
inventory. A wrapper's own upstream-tracking workflow is not a substitute
for the fleet consumer policy or its guarded host builds.
Update the lock deliberately for the new input, inspect the resulting diff, and do not manually float the deployment input to an unreviewed revision:
nix flake update <input-name>
git diff -- flake.nix flake.lock lib/auto-update-policy.nix
The update command is for an intentional packaging change in a feature
branch. Routine fleet auto-update follows the producer/consumer control plane
in Auto-Update Incident Response, not an
unconditional git push from a wrapper workflow.
3. Integrate the NixOS service
Import the fleet module from the consuming host and place reusable integration
in modules/services/. Declare the service user or DynamicUser, state
directory, restart policy, dependencies, devices/mount guards, listener,
firewall, backup contribution, and failure monitoring. Use the upstream module
where it fits rather than duplicating its options. Redview demonstrates a
mount guard and nixosConfig.backup.dataPaths; Navidrome MCP demonstrates a
Nebula-bound listener, device ordering, LoadCredential, and a monitored
systemd unit. These are patterns to adapt, not defaults to paste unchanged.
For secrets, create a service-scoped SOPS file under the correct .sops.yaml
recipient rule and consume it at runtime via sops.secrets, a SOPS template,
or systemd LoadCredential as appropriate. Name NixOS secret attributes
uniquely. Verify both the recipient scope and runtime file ownership; a
package must not embed secret values. Use Secret Editing and Recipient
Rotation for the current helper
commands and recovery rules.
Choose access from the actual client graph. A mesh-only service can bind to
nixosConfig.nebula.overlayIp and open only
networking.firewall.interfaces."nebula.mesh".allowedTCPPorts; it also needs
appropriate mesh-device ordering if the bind or upstream connection depends
on Nebula. A container on the same host reaches a mesh IP through its bridge,
so a mesh-interface-only firewall rule may not admit it. Do not widen to
0.0.0.0 or a global port merely to conceal that mismatch. Confirm ingress,
DNS, and authentication separately.
4. Validate, review, and stage deployment
Stage any new .nix file before flake evaluation, then run the applicable
checks from the source checkout:
just check
nix build .#nixosConfigurations.<host>.config.system.build.toplevel --no-link
Replace <host> with each consuming host. just check is nix flake check;
the host build verifies the actual service closure. Inspect the required
Attic producer and update-policy checks in the merge request as well as the
resulting package/version diff before a canary. just test-auto-update runs
remediation regressions; it is not the input-policy inventory gate. On an
affected NixOS host, use Safe Testing and
Deployment, including an update hold for a
remote test, just test for current-specialisation-preserving activation,
and a real client/health check. A wrapper build or flake evaluation is not
evidence that the live service, route, database, or backup works.
Success checks
- The wrapper builds the exact output the host consumes, and its tests and update/cache ownership are explicit.
flake.lockpins that output;lib/auto-update-policy.nixnames the input and correct consumers, or records a justified pin; CI and host build pass.- The canary has the expected package, runtime credentials, listener, mount, service health, client behavior, monitoring, and verified backup coverage.
- The issue records deployment and no-op follow-up evidence before a deployment-affecting change is closed.
Rollback and troubleshooting
For a package or module regression, stop promotion, inspect the rejected input/build evidence, and return to the last validated wrapper revision or NixOS generation through the normal review/deploy path. If the new version has migrated writable state, package rollback is not data rollback: restore a compatible service-specific snapshot/dump under the cutover plan. Keep the previous route and data recoverable until the acceptance window ends.
When a build fails, distinguish fixed-output hash mismatch, unsupported
architecture, missing runtime asset, broken upstream source pin, and a
consumer-only NixOS evaluation failure. When the process starts but clients
fail, check its own logs, exact proxy path behavior, bind/firewall interface,
mesh readiness, and credential paths before rebuilding the package. In
particular, an nginx proxy_pass trailing slash can rewrite a route prefix;
compare the actual upstream proxy contract before changing it.
Canonical source map
| Concern | Source |
|---|---|
| Placement rationale | ADR-002 |
Root inputs and specialArgs |
flake.nix |
| Input-to-consumer update policy | lib/auto-update-policy.nix |
| Producer controller source boundary | scripts/attic-cache-build.sh, pinned nixos-auto-update input |
| Fleet module examples | modules/services/redview.nix, modules/services/navidrome-mcp.nix |
| Deploy and secret helpers | justfiles/deploy.just, justfiles/secrets.just |
| Migration of an existing service | Service Migration and Cutover |