Build nixos systems from a local repository
In this post I'll show you how I manage 6 servers and 2 workstations from a single repository.
The repository
I use the old way, I've set up a repo at ~/.config/nixpkgs
.
In this repository there is all I need for deploying and accessing the servers.
The machines
So if you used ansible you'll find some similarities here. I have a file at the root of the repository:
# ./machines.nix
{
someName = {
name = "the name of the machine";
user = "root";
address = "the ip address of the machine (not for workstations)";
vpnAddress = "the vpn ip address";
hasSwitch = true; # true will generate the associated `switch_` command alias later
hasSSH = true; # true will generate the associated `ssh_` command alias later
wgPublicKey = ""; # used for wireguard
sshPublicKey = ""; # used for secrets (agenix)
};
}
In this file I can set up all my machines, and using these attributes, we'll see later how to generate de associated commands.
Agenix
I use agenix, so I have also a secrets file:
# ./secrets.nix
let
machines = import ./machines.nix;
# Physical machines
someMachine = machines.someMachine.sshPublicKey;
someOtherMachine = machines.someOtherMachine.sshPublicKey;
# workstation1 = ...
# [...]
# Roles
postgres-server = someMachine;
# Groups
locals = [ workstation1 workstation2 ];
postgres-server-group = [ postgres-server ovh01 ] ++ locals;
in
{
"secrets/postgres-server-masterpassword.age".publicKeys = postgres-server-group;
# [...]
}
Then I can inject/rotate secrets in all my servers/workstations.
The core: flake.nix
So now the most interesting part. I won't show any real nixos configuration because there is plenty of examples on the web, but just what I found useful to do here.
Brace yourself there is a lot coming.
{
inputs = {
nixpkgs-unstable.url = "nixpkgs/nixos-unstable"; # I used both stable and unstable so I've made 2 different variables
nixpkgs-stable.url = "nixpkgs/nixos-25.05";
home-manager-unstable.url = "github:nix-community/home-manager";
home-manager-unstable.inputs.nixpkgs.follows = "nixpkgs-unstable";
home-manager-stable.url = "github:nix-community/home-manager/release-25.05";
home-manager-stable.inputs.nixpkgs.follows = "nixpkgs-stable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{ self,
# [...]
} @inputs:
let
system = "x86_64-linux";
machines = import ./machines.nix;
hostsContent = (builtins.map
(x:
(if machines.${x}.hasSSH then [
"${machines.${x}.address} ${x}\n"
] else [ ])
)
(builtins.attrNames machines)); # Generate hosts files with named machines IPs
in
{
nixosConfigurations = {
workstation1 =
let
nixpkgs = nixpkgs-unstable;
home-manager = home-manager-unstable;
in
nixpkgs.lib.nixosSystem
(import ./nixos-workstation1.nix
{
inherit hostsContent;
vpnAddress = machines.workstation1.vpnAddress;
}
{
inherit home-manager; inherit inputs; inherit agenix; pkgs = nixpkgs;
});
# [...]
};
} // flake-utils.lib.eachDefaultSystem
(system:
let pkgs = nixpkgs-unstable.legacyPackages.${system};
in {
devShells.default = pkgs.mkShell
{
buildInputs = with pkgs; [
agenix.packages.${system}.default
# this command will nixos-rebuild switch all machines with hasSwitch and a vpnAddress
(writeShellScriptBin "switch_vpn_all"
(builtins.toString
(builtins.map
(x:
(if machines.${x}.hasSwitch && builtins.hasAttr "vpnAddress" machines.${x} then [
''
echo "[SWITCH ${x} at ${machines.${x}.user}@${machines.${x}.vpnAddress}]";
nixos-rebuild switch --target-host ${machines.${x}.user}@${machines.${x}.vpnAddress} --flake "./#${x}";
''
] else [ ])
)
(builtins.attrNames machines)))
)
# this command will nixos-rebuild switch all machines with hasSwitch and an address
(writeShellScriptBin "switch_all"
(builtins.toString
(builtins.map
(x:
(if machines.${x}.hasSwitch && builtins.hasAttr "address" machines.${x} then [
''
echo "[SWITCH ${x} at ${machines.${x}.user}@${machines.${x}.address}]";
nixos-rebuild switch --target-host ${machines.${x}.user}@${machines.${x}.address} --flake "./#${x}";
''
] else [ ])
)
(builtins.attrNames machines)))
)
# print all IPs and machines names
(writeShellScriptBin "all_ips"
(builtins.toString
(builtins.map
(x:
(if machines.${x}.hasSwitch then [
''
echo "LOCAL ${x}: ${machines.${x}.user}@${machines.${x}.address}"
''
] else [ ]) ++ (if builtins.hasAttr "vpnAddress" machines.${x} then [
''
echo "VPN ${x}: ${machines.${x}.user}@${machines.${x}.vpnAddress}"
''
] else [ ])
)
(builtins.attrNames machines)))
)
] ++
(builtins.map
(x:
(if machines.${x}.hasSwitch then [
# each machine can be switched individually
(writeShellScriptBin
"switch_${x}"
''
nixos-rebuild switch --target-host ${machines.${x}.user}@${machines.${x}.address} --flake "./#${x}"
'')
] else [ ]) ++
(if builtins.hasAttr "vpnAddress" machines.${x} && machines.${x}.hasSwitch then [
# each machine can be switched individually over vpn
(writeShellScriptBin
"switch_vpn_${x}"
''
nixos-rebuild switch --target-host ${machines.${x}.user}@${machines.${x}.vpnAddress} --flake "./#${x}"
'')
] else [ ]) ++
(if machines.${x}.hasSSH then [
# ssh commands like ssh_machine1
(writeShellScriptBin
"ssh_${x}"
''
ssh ${machines.${x}.user}@${machines.${x}.address}
'')
] else [ ]) ++
(if builtins.hasAttr "vpnAddress" machines.${x} && machines.${x}.hasSSH then
[
# ssh commands like ssh_vpn_machine1 for vpn
(writeShellScriptBin "ssh_vpn_${x}"
''
ssh ${machines.${x}.user}@${machines.${x}.vpnAddress}
'')
]
else [ ])
)
(builtins.attrNames machines));
};
});
}