Skip to content

Template: Fleet Demo

The fleet demo demonstrates Den’s multi-host capabilities: custom entity types, environment scoping, quirk-based data flow, and cross-host collection.

flake
+-- flake-system (x86_64-linux)
| +-- [packages, checks, devShells]
+-- fleet
+-- environment:prod
| +-- host:lb-prod (haproxy load balancer)
| +-- host:web-prod-1 (nginx backend)
| +-- host:web-prod-2 (nginx backend)
+-- environment:staging
+-- host:web-staging (nginx backend)

Hosts are grouped by environment. pipe.collect gathers data only from same-environment peers — the production load balancer sees only production backends.

The demo defines environment as a user-space entity type:

modules/environments.nix
den.schema.environment.isEntity = true;
options.fleet.environments = lib.mkOption {
type = lib.types.attrsOf environmentType;
};

No framework changes required — entity types are declared entirely in user flakes.

Three policies wire the scope tree:

modules/policies/fleet.nix
# flake -> fleet (single fleet entity)
den.policies.to-fleet = _: [ resolve.to "fleet" { fleet = { name = "fleet"; }; } ];
# fleet -> environments (one per registered environment)
den.policies.fleet-to-envs = { fleet, ... }:
lib.mapAttrsToList (_: env: resolve.to "environment" { environment = env; })
config.fleet.environments;
# environment -> hosts (hosts whose environment matches)
den.policies.env-to-hosts = { environment, ... }:
lib.concatMap (...) # resolve.to "host" + policy.instantiate per host

The default host-walking policies (to-os-outputs, to-hm-outputs) are excluded so the fleet topology controls the scope tree:

den.schema.flake-system.excludes = [
den.policies.to-os-outputs
den.policies.to-hm-outputs
];

Two quirks enable cross-host data flow:

QuirkProducerConsumerPurpose
http-backendsnginx aspectshaproxy aspectBackend server addresses
host-addrshostfile aspecthostfile aspect/etc/hosts entries

Producer — each nginx host emits its address:

modules/aspects/features/nginx.nix
den.aspects.nginx = {
nixos = { ... }: { services.nginx.enable = true; /* ... */ };
http-backends = { host, ... }: {
inherit (host) addr;
port = host.httpPort;
};
};

Collection policy — every host collects from peers:

modules/policies/pipes.nix
den.policies.collect-backends = { host, ... }: [
(pipe.from "http-backends" [
(pipe.collect ({ host, ... }: true))
])
];

Consumer — haproxy receives the collected data as a function argument:

modules/aspects/features/haproxy.nix
den.aspects.haproxy = {
nixos = { http-backends, lib, ... }: {
services.haproxy.config = /* generate config from http-backends */;
};
};

The pipeline automatically resolves parametric quirk values ({ host, ... }: ...) using each source scope’s context before delivering them.

templates/fleet-demo/
flake.nix
modules/
den.nix # hosts, defaults, systems
environments.nix # environment entity type + host schema extensions
flake-parts.nix # standard wiring
policies/
fleet.nix # fleet -> env -> host scope tree
pipes.nix # quirk declarations + collection policies
aspects/
features/
nginx.nix # web server + http-backends producer
haproxy.nix # load balancer (consumes http-backends)
hostfile.nix # /etc/hosts (consumes + produces host-addrs)
hosts/
lb-prod.nix # includes haproxy + hostfile
web-prod-1.nix # includes nginx + hostfile
web-prod-2.nix # includes nginx + hostfile
web-staging.nix # includes nginx + hostfile
users/
deploy.nix # deploy user (all hosts)
Terminal window
# Verify haproxy gets both production backends
nix eval --override-input den . \
./templates/fleet-demo#nixosConfigurations.lb-prod.config.services.haproxy.config
# Verify /etc/hosts has all production peers
nix eval --override-input den . \
./templates/fleet-demo#nixosConfigurations.web-prod-1.config.networking.extraHosts
FeatureShown
Custom entity types (environment)
Multi-level scope tree (fleet → env → host)
Quirk declarations (den.quirks)
Cross-host pipe.collect
Environment-scoped isolation
Parametric quirk values ({ host, ... }:)
Schema policy excludes
Contribute Community Sponsor