skip to content

A Small Deployment

A small deployment runs two NixOS hosts: web, a web server, and db, a database. Both need SSH and monitoring. Two operator accounts exist: deploy has admin rights on both hosts, and dba has an account on the database host only. Each feature is written once as an aspect, a named and reusable piece of configuration, and gen builds one NixOS system per host.

hostaspects it asks foraspects it getsusers
webwebserverwebserver, ssh, monitoring, basedeploy (wheel)
dbdatabasedatabase, ssh, monitoring, basedeploy (wheel), dba
  • flake.nix
  • aspect-cnf.nix
  • Directorygen-modules/
    • deployment.nix
  • Directoryhardware/
    • web.nix
    • db.nix
flake.nix
{
inputs = {
gen.url = "github:sini/gen";
nixpkgs.follows = "gen/nixpkgs";
flake-parts.url = "github:hercules-ci/flake-parts";
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
};
outputs = inputs:
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ inputs.gen.flakeModules.default ];
systems = [ "x86_64-linux" ];
gen = {
tree = ./gen-modules;
aspectCnf = import ./aspect-cnf.nix;
nodeRegistryPath = [ "hosts" ];
extraModules.web = [ ./hardware/web.nix ];
extraModules.db = [ ./hardware/db.nix ];
};
};
}
aspect-cnf.nix
{
keySemantics.nixos.category = "class";
}
# hardware/web.nix (db.nix is the same)
{
nixpkgs.hostPlatform = "x86_64-linux";
boot.loader.grub.enable = false;
fileSystems."/" = { device = "none"; fsType = "tmpfs"; };
}

The hub options are listed in Add gen to a Flake.

gen-modules/deployment.nix
{ config, genAspects, genMerge, genSchema, ... }:
let
inherit (genMerge) mkOption types;
aspectSchema = genAspects.mkAspectSchema (import ../aspect-cnf.nix);
# Two kinds.
schema = genSchema.evalSchema { schemaOption = aspectSchema.schemaOption; } [ {
config.schema.host = {
options.aspects = mkOption { type = types.listOf types.str; default = [ ]; };
};
config.schema.user = {
options.hosts = mkOption { type = types.listOf types.str; default = [ ]; };
options.admin = mkOption { type = types.bool; default = false; };
};
} ];
# The users that list a host.
usersOn = host:
builtins.filter (u: builtins.elem host config.users.${u}.hosts)
(builtins.attrNames config.users);
in
{
imports = [ (aspectSchema.mkAspectModule { }) ];
# Two registries.
options.hosts = genSchema.mkInstanceRegistry { } schema.host;
options.users = genSchema.mkInstanceRegistry { } schema.user;
config = {
hosts.web.aspects = [ "webserver" ];
hosts.db.aspects = [ "database" ];
users.deploy = { hosts = [ "web" "db" ]; admin = true; };
users.dba.hosts = [ "db" ];
aspects.base.nixos = { node, ... }: {
networking.hostName = node.name;
users.users = builtins.listToAttrs (map (u: {
name = u;
value = {
isNormalUser = true;
extraGroups = if config.users.${u}.admin then [ "wheel" ] else [ ];
};
}) (usersOn node.name));
system.stateVersion = "25.05";
};
aspects.ssh.includes = [ "base" ];
aspects.ssh.nixos.services.openssh.enable = true;
aspects.monitoring.includes = [ "base" ];
aspects.monitoring.nixos.services.prometheus.exporters.node.enable = true;
aspects.webserver.includes = [ "ssh" "monitoring" ];
aspects.webserver.nixos.services.nginx.enable = true;
aspects.database.includes = [ "ssh" "monitoring" ];
aspects.database.nixos.services.postgresql.enable = true;
};
}
Terminal window
nix eval .#nixosConfigurations --apply builtins.attrNames
# [ "db" "web" ]
nix eval --json .#nixosConfigurations.web.config.services.nginx.enable
# true
nix eval --json .#nixosConfigurations.db.config.services.prometheus.exporters.node.enable
# true
nix eval --json .#nixosConfigurations.db.config.users.users.deploy.extraGroups
# ["wheel"]
nix build .#nixosConfigurations.db.config.system.build.toplevel

web runs nginx and db runs PostgreSQL. Both run SSH and the Prometheus node exporter, which they reach through includes. dba has an account on db only.

part of the examplewhat it isread more
hosts, users, aspects in one treenodes in one graph, related by aspects, hosts and includesYour Configuration as a Graph
aspects.webserver, includes = [ "ssh" "monitoring" ]reusable features that include each otherReusable Features
mkInstanceRegistry, node.nameinstances of a kind, each with a stable identityStable Identity for Everything
mkOption, types.listOf, evalSchemagen’s own typed module systemAn Enhanced Module Type System
{ node, ... }: in aspects.base.nixosthe host bound into the module by nameAutomatic Argument Injection
aspect-cnf.nix, nodeRegistryPath, nixosConfigurationsprojection and realization through a terminalDelivery

Each host lists only the aspects it asks for. Delivery follows includes itself: a host receives the content of its aspects and of everything they include, so web gets webserver, ssh, monitoring and base. gen-aspects’ graphFacts publishes the same includes relation as graph data for gen-graph queries. See Aspects.

palette
dark
light
↑↓ select↵ apply esc close

Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).