Skip to content

Multi-Environment Fleet

  • prod environment: one load balancer + two web servers
  • staging environment: one web server
  • haproxy on the load balancer, configured from sibling web nodes
  • /etc/hosts on every host, listing peers in the same environment
  • monitoring auto-injected on all servers via neededBy

This is a simplified version of templates/fleet-demo.

# Entity: produces nixosSystem
nest.trait.host.class.nixos =
select: cfg:
nixpkgs.lib.nixosSystem {
system = select.node.system;
modules = [ cfg ];
};
# Service traits — no output themselves
nest.trait.nginx = { };
nest.trait.ssh = { };
nest.trait.firewall = { };
# server bundles nginx + ssh + firewall
nest.trait.server.needs = [ nest.nginx nest.ssh nest.firewall ];
# lb and web both imply server
nest.trait.lb.needs = [ nest.server ];
nest.trait.web.needs = [ nest.server ];
# monitoring auto-injects on every server — no node needs to opt in
nest.trait.monitoring = { };
nest.trait.monitoring.neededBy = nest.server;
# Namespace attributes inherited by all prod nodes
nest.prod.system = "x86_64-linux";
nest.prod.env = "prod";
# prod load balancer
nest.prod.lb-prod = {
is = [ nest.host nest.lb ];
addr = "10.0.1.1";
httpPort = 80;
};
# prod web servers
nest.prod.web-prod-1 = {
is = [ nest.host nest.web ];
addr = "10.0.1.10";
httpPort = 80;
};
nest.prod.web-prod-2 = {
is = [ nest.host nest.web ];
addr = "10.0.1.11";
httpPort = 80;
};
# staging namespace
nest.staging.system = "x86_64-linux";
nest.staging.env = "staging";
nest.staging.web-staging = {
is = [ nest.host nest.web ];
addr = "10.0.2.10";
httpPort = 80;
};
nest.rules = [
# Every host: boot config and Nix settings
{
is = nest.host;
nixos = { host, ... }: {
networking.hostName = host.name;
system.stateVersion = "25.11";
nix.settings.experimental-features = [ "nix-command" "flakes" ];
};
}
# Service traits (auto-injected via server.needs)
{ is = nest.nginx; nixos.services.nginx.enable = true; }
{ is = nest.ssh; nixos.services.openssh.enable = true; }
{ is = nest.firewall; nixos.networking.firewall.enable = true; }
# Monitoring (auto-injected via monitoring.neededBy = server)
{
is = nest.monitoring;
nixos.services.prometheus.exporters.node = {
enable = true;
enabledCollectors = [ "systemd" ];
};
}
# Load balancer: discover web peers via select.siblings
{
is = nest.lb;
nixos = { select, ... }:
let
webs = select.siblings nest.web;
in {
services.haproxy.enable = true;
services.haproxy.config = mkHaproxyConfig
(map (w: { addr = w.addr; port = w.httpPort; }) webs);
};
}
# Every host: /etc/hosts from sibling hosts
{
is = nest.host;
nixos = { select, ... }:
let
peers = select.siblings nest.host;
in {
networking.extraHosts =
lib.concatMapStringsSep "\n" (p: "${p.addr} ${p.name}") peers;
};
}
];

select.siblings returns all nodes sharing the same __parentPath. Because prod and staging are namespaces (not DOM nodes), all top-level hosts have __parentPath = null — which means they are siblings of each other.

In the fleet-demo, however, the haproxy rule uses select.siblings nest.web. This returns all web-trait nodes with the same parent. Since all hosts share __parentPath = null, this includes web-staging as a backend for lb-prod.

After step 2 (trait expansion), lb-prod’s is list is:

[ nest.host, nest.lb, nest.server, nest.nginx, nest.ssh, nest.firewall ]

After step 3 (neededBy), nest.monitoring is added:

[ nest.host, nest.lb, nest.server, nest.nginx, nest.ssh, nest.firewall, nest.monitoring ]

Rules for nginx, ssh, firewall, and monitoring all fire on lb-prod automatically, because it has nest.server via lb.needs.

mkHaproxyConfig = backends:
lib.concatStringsSep "\n" ([
"frontend http-in"
" bind *:80"
" default_backend webservers"
""
"backend webservers"
" balance roundrobin"
] ++ lib.imap1 (i: b:
" server backend${toString i} ${b.addr}:${toString b.port} check"
) backends);

select.siblings nest.web is evaluated at nixosSystem build time — if you add a web server, the haproxy config updates without touching any lb rule.

After evaluation, byClass.nixos contains:

{
lb-prod = nixosSystem { haproxy config with 2 (or 3) web backends };
web-prod-1 = nixosSystem { nginx, ssh, firewall, monitoring };
web-prod-2 = nixosSystem { nginx, ssh, firewall, monitoring };
web-staging = nixosSystem { nginx, ssh, firewall, monitoring };
}

flake.nixosConfigurations = byClass.nixos makes them available as standard NixOS configurations.

Contribute Community Sponsor