Table of Contents
- Chapter 2: Common Workflows
- Workflow 1: Adding a System Package
- Workflow 2: Adding a User Package
- Workflow 3: Adding a Host-Specific Package
- Workflow 4: Deploying to a Specific Host
- Workflow 5: Adding an External Flake Package
- Workflow 6: Creating a New Module
- Workflow 7: Creating a Module with Options
- Workflow 8: Using an Electra Profile Pattern
- Workflow 9: Updating the System
- Trigger an update manually
- Check what changed after an update
- If the repo has uncommitted changes
- Check update status
- Workflow 10: Testing Without Deploying
- Workflow 11: Building Without Activating
- Workflow 12: Rolling Back
- Workflow 13: Removing a Package
- Workflow 14: Removing a Module
- Workflow 15: Checking Service Status
- Workflow 16: Setting a Manual Power Profile
- Workflow 17: Checking Host Load With Beszel
- The Golden Rule
Chapter 2: Common Workflows
Step-by-step instructions for every task you will do regularly. Each workflow includes the exact commands to run and the files to edit.
Workflow 1: Adding a System Package
Scenario: You want to install a new tool (e.g., tmux) for all users on all hosts.
Steps
-
Find the package name:
nix search nixpkgs tmuxOutput shows the attribute name (e.g.,
legacyPackages.x86_64-linux.tmux). The part after the last dot is what you use:tmux. -
Edit
modules/common/system.nixand add it to the list:environment.systemPackages = with pkgs; [ # ... existing packages ... tmux # Terminal multiplexer ]; -
Validate:
nix flake check -
Deploy on the current host:
nixos-rebuild-auto # or: sudo nixos-rebuild switch --flake .#electra -
Commit and push. The other hosts will pick it up automatically on their next overnight update (or you can rebuild them manually).
Workflow 2: Adding a User Package
Scenario: You want a package only for nimmo on all hosts (e.g., obsidian).
Steps
-
Edit
home/nimmo.nixand add it to thehome.packageslist:home.packages = with pkgs; [ # ... existing packages ... obsidian ]; -
Check if the package is unfree. If so, verify
nixpkgs.config.allowUnfree = true;is set (it already is in your config). -
Validate and deploy:
nix flake check nixos-rebuild-auto -
Commit and push.
Workflow 3: Adding a Host-Specific Package
Scenario: You want a package only on specific hosts.
Steps
-
All three electra tiers: Add it to
hosts/electra/default.nix(outside all specialisation blocks), or create/extend a profile inmodules/profiles/. -
Electra igpu and dgpu tiers only (not base): Add it to
specialisation.igpu.configurationandspecialisation.dgpu.configurationinhosts/electra/default.nix. -
Electra dgpu tier only: Add it inside
specialisation.dgpu.configurationinhosts/electra/default.nix. -
One specific host only: Edit that host's
default.nix(e.g.,hosts/lena/default.nix). -
Validate, deploy, commit, push.
Workflow 4: Deploying to a Specific Host
You can build any host's configuration from any machine in the repo:
# Deploy to the current machine
nixos-rebuild-auto
# Deploy to a specific host by name (must be run on that host, or via SSH)
sudo nixos-rebuild switch --flake .#electra
sudo nixos-rebuild switch --flake .#lena
Each host name must match the networking.hostName set in that host's config and the key in nixosConfigurations in flake.nix.
Workflow 5: Adding an External Flake Package
Scenario: You want to add a tool that is packaged as a Nix flake on GitHub.
Steps
-
Add the input to
flake.nix:inputs = { # ... existing inputs ... new-tool.url = "github:owner/repo"; new-tool.inputs.nixpkgs.follows = "nixpkgs"; }; -
The outputs function already captures everything via
... }@inputs:, so no changes to the outputs parameters are needed. -
Use it in the appropriate file:
# For a user package, in home/nimmo.nix: home.packages = with pkgs; [ inputs.new-tool.packages.${pkgs.stdenv.hostPlatform.system}.default ]; # For a system package, in modules/common/system.nix: environment.systemPackages = [ inputs.new-tool.packages.${pkgs.stdenv.hostPlatform.system}.default ];Note: you cannot use
with pkgs;for external flake packages. You must use the fullinputs.path. -
Update the lock file:
nix flake update new-tool -
Validate, deploy, commit, push.
Workflow 6: Creating a New Module
Scenario: You want to create a reusable module for SSH.
Steps
-
Create the module file:
# modules/services/ssh.nix { config, pkgs, ... }: { services.openssh = { enable = true; settings = { PasswordAuthentication = false; PermitRootLogin = "no"; }; }; networking.firewall.allowedTCPPorts = [ 22 ]; } -
Stage the new file (required for flakes):
git add modules/services/ssh.nix -
Import it in the host(s) that need it. For example, edit a host's
default.nix:imports = [ # ... existing imports ... ../../modules/services/ssh.nix ]; -
Validate:
nix flake check -
Deploy, commit, push.
Workflow 7: Creating a Module with Options
Scenario: You want a configurable module (e.g., a web server with a configurable port).
Steps
-
Create the module:
# modules/services/web-server.nix { config, lib, pkgs, ... }: let inherit (lib) mkEnableOption mkOption mkIf types; cfg = config.services.my-web-server; in { options.services.my-web-server = { enable = mkEnableOption "my web server"; port = mkOption { type = types.port; default = 8080; description = "Port to listen on"; }; }; config = mkIf cfg.enable { networking.firewall.allowedTCPPorts = [ cfg.port ]; }; } -
Stage it:
git add modules/services/web-server.nix -
Import and configure in a host:
imports = [ ../../modules/services/web-server.nix ]; services.my-web-server = { enable = true; port = 9090; }; -
Validate, deploy, commit, push.
Workflow 8: Using an Electra Profile Pattern
Scenario: You want to add capability to electra's igpu and dgpu tiers but not the base battery tier.
Profiles are the cleanest way to add opt-in capability. Look at modules/profiles/gaming.nix as an example:
# modules/profiles/gaming.nix
{ config, lib, pkgs, inputs, ... }:
let
user = config.nixosConfig.primaryUser;
in {
programs.steam.enable = true;
programs.gamemode.enable = true;
home-manager.users.${user}.home.packages = with pkgs; [
mangohud
discord
];
}
In hosts/electra/default.nix, it is imported inside both specialisations:
specialisation.igpu.configuration = {
imports = [
../../modules/profiles/gaming.nix
# ...
];
};
specialisation.dgpu.configuration = {
imports = [
../../modules/profiles/gaming.nix
# ...
];
};
This keeps the base (battery) tier clean — no Steam, no Discord, no gaming overhead.
Workflow 9: Updating the System
Updates run automatically via the nixos-auto-update systemd service. All configured hosts currently run in pull-only mode: they pull the latest repo state from git and rebuild. The flake.lock is updated manually or by the external cache/update workflow, then client machines consume that pushed change.
Trigger an update manually
just auto-update-run-now
The service respects a 6-hour freshness window — if flake.lock was updated less than 6 hours ago it will skip the flake update and exit cleanly. To force an update regardless:
sudo nixos-auto-update --force
Check what changed after an update
just diff-last-update
This compares the previous and current system generations using nix store diff-closures, showing which packages changed version.
If the repo has uncommitted changes
The service will skip itself rather than interfere with work in progress. Once you have committed your changes, trigger it manually or wait for the next scheduled run. For manual pulls before committing, use:
git pull --rebase --autostash
This replaces the old manual stash/pull/pop habit; Git creates and reapplies the temporary stash as part of the rebase.
Check update status
just auto-update-last-run # Last run result and timestamp
journalctl -u nixos-auto-update.service -f # Live log output
cat /var/log/nixos-auto-update-error.log # Error detail if last run failed
Workflow 10: Testing Without Deploying
Scenario: You want to try a change without adding it to your boot menu.
sudo nixos-rebuild test --flake .#electra
This builds and activates the new configuration, but does not add it to the boot menu. On your next reboot, the system reverts to the previously deployed configuration.
Use test for:
- Trying out new services
- Testing hardware configuration changes
- Verifying a change works before committing to it
Once you are satisfied, run switch to make it permanent.
Workflow 11: Building Without Activating
Scenario: You want to verify a configuration builds successfully without changing your running system at all.
sudo nixos-rebuild build --flake .#electra
This builds the configuration and puts the result in ./result, but does not activate it and does not add it to the boot menu. Useful for checking that an update does not break the build.
You can also inspect what the build contains:
ls result/specialisation/ # Shows igpu and dgpu specialisation outputs
Workflow 12: Rolling Back
From the boot menu
Reboot and select a previous generation from the boot menu. This is the simplest method and works even if the current generation is completely broken.
From the command line
# List available generations
sudo nix-env --list-generations --profile /nix/var/nix/profiles/system
# Switch to a specific generation
sudo nix-env --switch-generation 42 --profile /nix/var/nix/profiles/system
sudo /nix/var/nix/profiles/system/bin/switch-to-configuration switch
After a bad update
If a nix flake update broke something:
# Revert the lock file
git log --oneline flake.lock # Find the last good commit
git checkout <commit-hash> -- flake.lock
# Rebuild with the reverted lock
nixos-rebuild-auto
# Commit the revert
git add flake.lock
git commit -m "revert: Roll back flake inputs to <commit-hash>"
git push
Workflow 13: Removing a Package
-
Remove the package from whichever file it is in (
modules/common/system.nix,home/nimmo.nix, or a host'sdefault.nix). -
Validate and deploy:
nix flake check nixos-rebuild-auto -
The package will be removed from your PATH immediately.
-
The package remains in the Nix store until garbage collection removes it. To clean up immediately:
sudo nix-collect-garbage
Workflow 14: Removing a Module
-
Remove the import from the host's
default.nix:imports = [ # ../../modules/services/ssh.nix # Removed ]; -
Remove any option settings for that module (if the module defined custom options):
# Remove these lines: # services.my-custom-service.enable = true; # services.my-custom-service.port = 9090; -
Validate and deploy. The service/feature will be disabled.
-
Optionally, delete the module file if no other host uses it:
git rm modules/services/ssh.nix
Workflow 15: Checking Service Status
Many modules create systemd services. Check their status:
# Is the service running?
systemctl status auto-power-profile.service
# View logs
journalctl -u auto-power-profile.service -f
# List all timers
systemctl list-timers
# Check a specific timer
systemctl status nix-gc.timer
Workflow 16: Setting a Manual Power Profile
Electra and other laptop hosts use modules/power/auto-power-profile.nix for automatic profile switching. You can temporarily override the selected profile with:
just power-profile performance
just power-profile balanced
just power-profile powersave
just power-profile auto
Manual mode is runtime state under /run/auto-power-profile. It is intentionally temporary: when battery capacity reaches 25% or lower, the service switches back to automatic mode so low-battery protection takes over.
In automatic mode:
dynamicstrategy follows AC/battery thresholds.endurancestrategy always targetspower-saver.
Electra base uses endurance; Electra igpu and dgpu, and Lena, use dynamic.
Workflow 17: Checking Host Load With Beszel
For Docker stack placement and load-balancing decisions, use the local Beszel helper:
/home/nimmo/Scripts/beszel-agent/beszel-agent status
/home/nimmo/Scripts/beszel-agent/beszel-agent nova
The overview shows host status, CPU, RAM, and uptime. A host drill-down includes disk pressure, temperatures, failed services, and running containers, which makes it useful before recommending where to move Docker stacks.
The Golden Rule
Always validate before deploying:
nix flake check # Does not need root, catches errors
nixos-rebuild-auto # Or: sudo nixos-rebuild switch --flake .#electra
Never skip nix flake check. It catches most errors in seconds without touching your running system.