Table of contents
Adding or Migrating a Service
- Type: Runbook
- Status: Current
- Scope: Engineering the declared runtime and fleet integration for a new or migrated service; live cutover is separate
- Canonical sources:
AGENTS.md;flake.nix;hosts/*/default.nix;modules/services/;modules/containers/;modules/services/backrest.nix;modules/services/service-failure-monitor.nix;lib/auto-update-policy.nix;.sops.yaml;justfiles/deploy.just; ADR-002; Service Migration and Cutover- Last verified: Source commit
556df88494686003b1c4f20c8e0b99b5afc16a6e(2026-09-12); source HEADb2831d33132967b4c4fb69e6068b2b05fff8fdffchanges onlyflake.lock. Service examples and validation contract checked 2026-09-13; no service was deployed or migrated.- Review triggers: Workload-placement policy, service/container modules, host imports, SOPS delivery, storage/backup, ingress, updater input policy, validation recipes, or service migration procedure
Purpose and boundary
Make a service's runtime, dependencies, state, access, and operational ownership reviewable in Nix. This page covers implementation and pre-deployment evidence. It does not authorise a data move, route change, shutdown of an old instance, or production deployment. For an existing service, Service Migration and Cutover owns the one-writer handover, verified recovery point, external ingress work, and rollback window.
Service changes normally need a Forgejo issue, feature branch, and merge
request under AGENTS.md. Record placement, affected hosts and clients,
acceptance checks, data authority, security exposure, deployment order, and
rollback in the issue. Use ADR-002 to choose
native NixOS, an independent wrapper flake, or OCI by operational fit—not by
a blanket migration target. Consult the Service Catalogue
for declared placement; configuration is not proof of a healthy live service.
Prerequisites and safety boundary
- Identify the owning host, service user, exact persistent paths and databases, mount origins, readers and writers, devices/GPU, upstream version, and update owner. For a migration, identify the old runtime and schema compatibility before building a target against copied state.
- Map listeners, firewall interfaces, reverse-proxy routes, DNS/TLS, and authentication against the Network and Access Topology. A loopback or mesh bind is not a public-ingress declaration; external Pangolin/Newt and DNS changes need their own owner and evidence.
- List every secret, its consuming unit, SOPS file/recipient scope, and runtime delivery path. Never place plaintext credentials in Nix expressions, the Nix store, Git, issue bodies, or documentation.
- Establish backup scope and an actual usable recovery route before a
stateful live cutover. Adding
nixosConfig.backup.dataPathsdeclares backup intent; it does not prove a successful snapshot or include a nested NAS mount. Keep old data and volumes recoverable through the agreed window.
Ordered procedure
1. Choose and document the ownership boundary
Use an upstream/nixpkgs NixOS service when its module expresses the required
runtime. Put thin fleet integration in modules/services/, and import and
enable it only from intended hosts/<host>/default.nix entries. An option-gated
module is useful when several hosts need different values; a direct import
can be enough for one owner. Follow Adding a Module or
Profile for the module interface.
If substantial reusable packaging, its own CI/cache build, or independent
release cadence is needed, follow Wrapper-Repository
Packaging. The external repository owns
its package and reusable module; this repository pins the flake input and
owns host integration. modules/services/redview.nix demonstrates the
boundary: it imports the external module and package, then declares Lyra's
GPU assertion, media mount, SOPS inputs, backup path, and failure monitor.
If an official image and container topology are the right boundary, declare
the container in modules/containers/ or the relevant host module with
explicit image/update behaviour, ports, volumes, networks, and systemd
relationships. modules/containers/paseo-webui.nix is an OCI example
imported by Chaos: it publishes a loopback-bound host port and restarts on
configuration revision. Do not assume an image tag or mutable application
settings are reproducible Nix state.
2. Declare the runtime contract
Specify the service user/group, package or image, persistent state directory,
database, environment, startup dependencies, mounts, resource/device needs,
and update behaviour. Assert prerequisites that must hold. For network or NAS
state, order the unit after the mount and fail before startup when the
expected filesystem is absent; modules/services/jellyfin.nix and
modules/services/redview.nix show explicit mount dependencies/guards.
Avoid writing application data onto the underlying mountpoint if a remote
mount is missing.
Keep secrets in service-scoped SOPS files with only required recipients.
Deliver them at runtime through sops.secrets paths, SOPS templates,
LoadCredential, or an environment file appropriate to the service; the
BookStack, Redview, and Navidrome MCP modules show different current patterns.
Review file ownership and modes as well as the process that reads them.
Follow Secret Editing and Recipient
Rotation for key/recipient work.
Bind to the narrowest required interface, open only the matching firewall
path, and name the ingress component separately. Declare health/failure
monitoring and add host-owned persistent paths to
nixosConfig.backup.dataPaths; verify the resulting backup plan and a
readable restore, especially across filesystems. The Security
Baseline and Storage and Data
Map own the wider review gates.
If a new root flake input is introduced, classify its actual consuming hosts
in lib/auto-update-policy.nix and review cache/build cost. Do not leave a
new update source unguarded or assign every host without evidence. Update
README.md and the relevant service atlas pages when the implementation
changes their described contract.
3. Validate without claiming a cutover
Stage new .nix files before flake evaluation. From the source checkout,
run just check and a non-activating build for each affected target:
just check
nix build .#nixosConfigurations.HOST.config.system.build.toplevel --no-link
Replace HOST with a registered target. Review the evaluated unit/container,
bind/firewall, state paths, mount ordering, SOPS references, update-policy
coverage, and build diff. Check required MR/CI results. On an affected NixOS
host, use just test for a canary; on a remote target first create an
auto-update hold. just test preserves Electra's active specialisation by
default. Exercise the real client workflow and inspect logs and backup
read-back, not just unit activation. Record exact validation and deployment
evidence in the issue. A local build cannot prove the target is deployed.
For a migration, now hand off to Service Migration and Cutover. Keep one authoritative writer, perform a consistent export/import, move routes deliberately, and verify the new service and restore path before retiring old state. Do not prune old container volumes merely because the new unit starts.
Success checks, rollback, and troubleshooting
- The intended host imports and enables exactly the owned service; placement and any external package/image responsibility are documented.
- Runtime identity, state, mount guard, secret delivery, listener/firewall, ingress owner, failure monitoring, backup intent, and update policy are explicit. Real backup and client checks are recorded before declaring live success.
just check, each affected target build, required MR checks, and an appropriate canary pass. For multi-host or auto-update changes, record a genuine deployment and no-op follow-up before closing the issue.- Before live deployment, revert the reviewed source change if necessary. After data migration or schema upgrade, a Nix generation rollback alone may be unsafe; use the service-specific recovery point and route rollback in the cutover issue/runbook.
- If evaluation fails, check Git staging, host imports, module options, and
flake-input policy. If startup fails, check
journalctlfor the unit, required mount presence, SOPS file permissions, and service dependencies. If local health works but clients fail, inspect listener, interface firewall, ingress route, DNS/TLS, and authentication separately. If backup verification fails, stop before cutover rather than treating the declared path as protection.
Canonical source map
| Concern | Source |
|---|---|
| Workflow, review, canary, and publication gates | AGENTS.md, justfiles/deploy.just, Safe Testing and Deployment |
| Placement and external packaging | ADR-002, Wrapper-Repository Packaging |
| Native and external-module examples | modules/services/bookstack.nix, modules/services/redview.nix, modules/services/jellyfin.nix, modules/services/navidrome-mcp.nix |
| OCI and host ownership examples | modules/containers/paseo-webui.nix, hosts/chaos/default.nix, hosts/vega/default.nix, hosts/lyra/default.nix |
| Backup, monitoring, update, and secrets | modules/services/backrest.nix, modules/services/service-failure-monitor.nix, lib/auto-update-policy.nix, .sops.yaml |
| Live data and traffic handover | Service Migration and Cutover |