Managing more than one Linux machine usually turns into a sync nightmare. You tweak a config on your desktop, forget to copy it to your laptop, and suddenly half your keybinds and scripts are out of date.
In my setup, I manage my desktop workstation (junixos) and laptop (junixbook) from a single Git repository using Nix Flakes. Here is how the structure works in practice—from custom option namespaces to zero-friction secrets and automated USB backups.
1. Auto-Importing Overlays and Flake Structure
Instead of manually listing dozens of overlay files in flake.nix, I wrote a simple helper that reads the modules/overlays/ directory and imports any .nix file it finds:
# flake.nix snippet
{
outputs = { self, nixpkgs, ... }@inputs:
let
loadOverlays = dir:
if builtins.pathExists dir then
let
files = builtins.attrNames (builtins.readDir dir);
nixFiles = builtins.filter (name: builtins.match ".*\\.nix" name != null) files;
in
map (name: import (dir + "/${name}")) nixFiles
else [ ];
myOverlays = loadOverlays ./modules/overlays;
in {
nixosConfigurations."junixos" = nixpkgs.lib.nixosSystem {
specialArgs = { inherit inputs; };
modules = [
{ nixpkgs.overlays = myOverlays; }
inputs.disko.nixosModules.disko
./hosts/junixos/disk-config.nix
./hosts/junixos/configuration.nix
inputs.sops-nix.nixosModules.sops
inputs.home-manager.nixosModules.default
inputs.nixvim.nixosModules.nixvim
];
};
};
}
2. The custom.* Namespace Approach
One trap in NixOS is writing raw configuration blocks everywhere. When you configure services directly in host files, things quickly become messy.
Instead, I define reusable modules under a custom.* hierarchy. A host configuration ends up looking clean and intentional:
# hosts/junixos/configuration.nix
{
custom = {
system = {
kernel = "cachyos";
boot.silent.enable = true;
android.enable = true;
};
desktop = {
hyprland.enable = true;
gaming = {
enable = true;
isolated.enable = false;
};
};
services = {
backup = {
enable = true;
deviceUuid = "9e2b10a4-3a21-4f12-88ec-123456789abc";
autoTriggerOnPlug = true;
};
zapret.enable = true;
};
};
}
3. Secrets Without GPG Headaches (sops-nix + SSH Keys)
I didn't want to deal with GPG keys or manual password manager exports when bootstrapping a fresh machine.
Using sops-nix, the system converts the machine's existing SSH host key (/etc/ssh/ssh_host_ed25519_key) into an age identity. All encrypted secret YAML files live in secrets/ inside the repo, and Nix decrypts them automatically at runtime:
# modules/nixos/sops.nix
{ config, pkgs, ... }:
let
secretsDir = ../../secrets;
hostSecretsFile = secretsDir + "/${config.networking.hostName}.yaml";
commonSecretsFile = secretsDir + "/common.yaml";
in {
sops = {
defaultSopsFormat = "yaml";
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
defaultSopsFile =
if builtins.pathExists hostSecretsFile then hostSecretsFile
else if builtins.pathExists commonSecretsFile then commonSecretsFile
else null;
};
environment.systemPackages = with pkgs; [ sops age ssh-to-age ];
}
4. Automated BorgBackups on USB Drive Insertion
Manual backups get forgotten. To fix this, I set up a BorgBackup service that triggers via a udev rule whenever I plug in my external backup drive:
# modules/services/backup.nix
{ config, lib, ... }:
let
cfg = config.custom.services.backup;
in {
config = lib.mkIf cfg.enable {
# Automount disk on demand with a 15s spin-up timeout
fileSystems."${cfg.mountPoint}" = {
device = "/dev/disk/by-uuid/${cfg.deviceUuid}";
fsType = "ext4";
options = [ "noauto" "nofail" "x-systemd.automount" "x-systemd.device-timeout=15s" ];
};
# Udev rule: triggers systemd backup service as soon as the drive is plugged in
services.udev.extraRules = lib.mkIf cfg.autoTriggerOnPlug ''
ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_UUID}=="${cfg.deviceUuid}", TAG+="systemd", ENV{SYSTEMD_WANTS}+="borgbackup-job-systemBackup.service"
'';
services.borgbackup.jobs.systemBackup = {
paths = [ "/home/${config.custom.user}" "/etc/ssh" "/var/lib/sops-nix" ];
repo = "${cfg.mountPoint}/${cfg.repoName}";
encryption.mode = "repokey-blake2";
encryption.passCommand = "cat ${config.sops.secrets.borg_passphrase.path}";
compression = "auto,zstd,3";
};
};
}
Whenever I plug in the drive, systemd automounts the partition, runs the encrypted incremental backup, and unmounts it when finished.
Summary
Having a unified multi-host flake means I can spin up a fresh machine or sync changes across hardware in a few minutes without guessing what packages were installed.
The complete configuration, Neovim modules, and Hyprland setup are available on GitHub in my NixOS-Dotfiles repository.