3 02 Workflows
Nimmo edited this page 2026-05-30 22:40:08 +01:00

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

  1. Find the package name:

    nix search nixpkgs tmux
    

    Output shows the attribute name (e.g., legacyPackages.x86_64-linux.tmux). The part after the last dot is what you use: tmux.

  2. Edit modules/common/system.nix and add it to the list:

    environment.systemPackages = with pkgs; [
      # ... existing packages ...
      tmux          # Terminal multiplexer
    ];
    
  3. Validate:

    nix flake check
    
  4. Deploy on the current host:

    nixos-rebuild-auto
    # or: sudo nixos-rebuild switch --flake .#electra
    
  5. 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

  1. Edit home/nimmo.nix and add it to the home.packages list:

    home.packages = with pkgs; [
      # ... existing packages ...
      obsidian
    ];
    
  2. Check if the package is unfree. If so, verify nixpkgs.config.allowUnfree = true; is set (it already is in your config).

  3. Validate and deploy:

    nix flake check
    nixos-rebuild-auto
    
  4. Commit and push.

Workflow 3: Adding a Host-Specific Package

Scenario: You want a package only on specific hosts.

Steps

  1. All three electra tiers: Add it to hosts/electra/default.nix (outside all specialisation blocks), or create/extend a profile in modules/profiles/.

  2. Electra igpu and dgpu tiers only (not base): Add it to specialisation.igpu.configuration and specialisation.dgpu.configuration in hosts/electra/default.nix.

  3. Electra dgpu tier only: Add it inside specialisation.dgpu.configuration in hosts/electra/default.nix.

  4. One specific host only: Edit that host's default.nix (e.g., hosts/lena/default.nix).

  5. 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

  1. Add the input to flake.nix:

    inputs = {
      # ... existing inputs ...
      new-tool.url = "github:owner/repo";
      new-tool.inputs.nixpkgs.follows = "nixpkgs";
    };
    
  2. The outputs function already captures everything via ... }@inputs:, so no changes to the outputs parameters are needed.

  3. 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 full inputs. path.

  4. Update the lock file:

    nix flake update new-tool
    
  5. Validate, deploy, commit, push.

Workflow 6: Creating a New Module

Scenario: You want to create a reusable module for SSH.

Steps

  1. Create the module file:

    # modules/services/ssh.nix
    { config, pkgs, ... }:
    
    {
      services.openssh = {
        enable = true;
        settings = {
          PasswordAuthentication = false;
          PermitRootLogin = "no";
        };
      };
    
      networking.firewall.allowedTCPPorts = [ 22 ];
    }
    
  2. Stage the new file (required for flakes):

    git add modules/services/ssh.nix
    
  3. Import it in the host(s) that need it. For example, edit a host's default.nix:

    imports = [
      # ... existing imports ...
      ../../modules/services/ssh.nix
    ];
    
  4. Validate:

    nix flake check
    
  5. 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

  1. 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 ];
      };
    }
    
  2. Stage it:

    git add modules/services/web-server.nix
    
  3. Import and configure in a host:

    imports = [
      ../../modules/services/web-server.nix
    ];
    
    services.my-web-server = {
      enable = true;
      port = 9090;
    };
    
  4. 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

  1. Remove the package from whichever file it is in (modules/common/system.nix, home/nimmo.nix, or a host's default.nix).

  2. Validate and deploy:

    nix flake check
    nixos-rebuild-auto
    
  3. The package will be removed from your PATH immediately.

  4. 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

  1. Remove the import from the host's default.nix:

    imports = [
      # ../../modules/services/ssh.nix   # Removed
    ];
    
  2. 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;
    
  3. Validate and deploy. The service/feature will be disabled.

  4. 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:

  • dynamic strategy follows AC/battery thresholds.
  • endurance strategy always targets power-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.