Polish ringtail NixOS config and add documentation (#208)

## Summary
- Fix Super+Return keybinding to launch wezterm in sway
- Set fish as default login shell
- Remove `initialPassword` (real password already set)
- Add 1Password CLI + GUI, chezmoi, and dev tool packages (neovim, eza, fd, fzf, zoxide, starship, atuin, bat, ripgrep)
- Add ringtail reference card, update host inventory and reference index
- Changelog fragment

## Post-merge deployment
- `mise run provision-ringtail` to rebuild NixOS
- On ringtail: launch 1Password GUI, enable CLI integration (Settings > Developer > CLI integration)
- Chezmoi needs `.chezmoiignore` updates in the dotfiles repo (separate task)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/208
This commit is contained in:
Erich Blume 2026-02-18 17:53:47 -08:00
commit 535f897054
11 changed files with 413 additions and 8 deletions

View file

@ -1,6 +1,8 @@
import dagger import dagger
from dagger import dag, function, object_type from dagger import dag, function, object_type
NIX_IMAGE = "nixos/nix:2.33.3"
@object_type @object_type
class BlumeopsCi: class BlumeopsCi:
@ -67,3 +69,26 @@ class BlumeopsCi:
) )
.file(f"/docs-{version}.tar.gz") .file(f"/docs-{version}.tar.gz")
) )
@function
async def flake_lock(
self, src: dagger.Directory, flake_path: str = "nixos/ringtail"
) -> dagger.File:
"""Resolve flake inputs and return updated flake.lock."""
return await (
dag.container()
.from_(NIX_IMAGE)
.with_directory("/workspace", src)
.with_workdir(f"/workspace/{flake_path}")
.with_exec(
[
"nix",
"--extra-experimental-features",
"nix-command flakes",
"flake",
"lock",
"--accept-flake-config",
]
)
.file(f"/workspace/{flake_path}/flake.lock")
)

View file

@ -8,7 +8,7 @@
ansible.builtin.git: ansible.builtin.git:
repo: "https://forge.ops.eblu.me/eblume/blumeops.git" repo: "https://forge.ops.eblu.me/eblume/blumeops.git"
dest: /etc/blumeops dest: /etc/blumeops
version: main version: "{{ ringtail_commit | default('main') }}"
force: true force: true
register: _repo register: _repo
@ -16,7 +16,7 @@
ansible.builtin.command: ansible.builtin.command:
cmd: nixos-rebuild switch --flake /etc/blumeops/nixos/ringtail#ringtail cmd: nixos-rebuild switch --flake /etc/blumeops/nixos/ringtail#ringtail
register: _rebuild register: _rebuild
changed_when: "'activating the configuration' in _rebuild.stdout" changed_when: "'activating the configuration' in _rebuild.stderr"
when: _repo.changed when: _repo.changed
- name: Verify tailscale is connected - name: Verify tailscale is connected

View file

@ -0,0 +1 @@
Ringtail post-install: NixOS config (sway with Catppuccin Macchiato theme, fish, 1Password, Steam, LibreWolf, Bluetooth audio, chezmoi, dev tools, nix-ld), Dagger flake-lock pipeline, improved provision-ringtail workflow, services-check integration, and reference documentation.

View file

@ -1,6 +1,6 @@
--- ---
title: Hosts title: Hosts
modified: 2026-02-10 modified: 2026-02-18
tags: tags:
- infrastructure - infrastructure
--- ---
@ -16,6 +16,7 @@ All devices connected via [Tailscale](https://login.tailscale.com/) tailnet `tai
| **Indri** | Mac Mini M1, 2020 - Primary server | [[indri|Details]] | | **Indri** | Mac Mini M1, 2020 - Primary server | [[indri|Details]] |
| **Gilbert** | MacBook Air M4, 2025 - Workstation | [[gilbert|Details]] | | **Gilbert** | MacBook Air M4, 2025 - Workstation | [[gilbert|Details]] |
| **[[sifaka|Sifaka]]** | Synology NAS - Storage & backups | [[sifaka|Details]] | | **[[sifaka|Sifaka]]** | Synology NAS - Storage & backups | [[sifaka|Details]] |
| **[[ringtail|Ringtail]]** | Custom PC, NixOS - Service host & gaming | [[ringtail|Details]] |
| **Mouse** | MacBook Air M2 - Allison's laptop | - | | **Mouse** | MacBook Air M2 - Allison's laptop | - |
| **UniFi** | UniFi Express 7 - Home WiFi | [[unifi|Details]] | | **UniFi** | UniFi Express 7 - Home WiFi | [[unifi|Details]] |
| **Dwarf** | iPad Air - Employer-provided, off tailnet | - | | **Dwarf** | iPad Air - Employer-provided, off tailnet | - |

View file

@ -0,0 +1,61 @@
---
title: Ringtail
modified: 2026-02-18
tags:
- infrastructure
- host
---
# Ringtail
Service host and gaming PC. Custom-built PC running NixOS.
## Specifications
| Property | Value |
|----------|-------|
| **Motherboard** | ASUS ROG Crosshair VI Hero (Wi-Fi AC) |
| **CPU** | AMD Ryzen 7 1700X (8-core/16-thread, 3.4 GHz) |
| **RAM** | 32 GB DDR4 (4x8 GB Corsair Vengeance CMK16GX4M2B3200C16, 3200 MT/s DOCP) |
| **GPU** | NVIDIA GeForce RTX 4080 (AD103, 16 GB VRAM) |
| **Monitor** | HP OMEN 27i IPS (2560x1440, 165 Hz, DisplayPort) |
| **Storage (boot)** | Samsung 970 PRO 1TB NVMe |
| **Storage (SATA)** | Samsung 850 EVO 1TB (`/mnt/games`), 850 EVO 500GB (`/mnt/storage1`), 840 PRO 120GB (`/mnt/storage2`) |
| **Peripherals** | Das Keyboard 4, Logitech MX Master 3, 8BitDo Ultimate 2 controller |
| **OS** | NixOS 25.11 (Sway/Wayland) |
| **Tailscale hostname** | `ringtail.tail8d86e.ts.net` |
## Software
Managed declaratively via `nixos/ringtail/configuration.nix`. Home-manager handles ringtail-specific sway/waybar config; chezmoi manages cross-platform dotfiles.
- **Desktop:** Sway (Wayland, Catppuccin Macchiato theme) with waybar and wezterm
- **Browser:** LibreWolf
- **Gaming:** Steam (library on `/mnt/games`), 8BitDo controller via Steam Input
- **Audio:** Edifier R1280DBs (Bluetooth), PipeWire
- **Secrets:** 1Password CLI + GUI (NixOS modules for polkit/setgid integration)
- **Runtimes:** mise manages Node, Python, Rust, .NET; nix-ld enables dynamically linked binaries
- **Dotfiles:** `chezmoi init eblume && chezmoi apply`
## Deployment
```fish
mise run provision-ringtail
```
This updates `flake.lock` via Dagger, verifies the current commit is pushed to forge, then deploys the exact commit via ansible. If the lockfile changed, it stages the file and exits so you can commit and re-run.
## Maintenance Notes
**1Password:** Desktop app must be running for `op` CLI. Use `$mod+Shift+minus` to send to scratchpad.
**NVIDIA:** Proprietary drivers. Sway launched with `--unsupported-gpu` via greetd.
**No TPM:** `systemd.tpm2.enable = false` prevents 90s boot delay.
**RAM speed:** Running at 3200 MT/s via DOCP 1 (BIOS 8902+).
## Related
- [[hosts]] - Device inventory
- [[tailscale]] - Network configuration

View file

@ -47,6 +47,7 @@ Host inventory and network configuration.
- [[hosts|Hosts]] - Device inventory - [[hosts|Hosts]] - Device inventory
- [[indri]] - Primary server - [[indri]] - Primary server
- [[ringtail]] - Service host & gaming PC
- [[gilbert]] - Development workstation - [[gilbert]] - Development workstation
- [[tailscale]] - ACLs, groups, tags - [[tailscale]] - ACLs, groups, tags
- [[gandi]] - DNS hosting for `eblu.me` - [[gandi]] - DNS hosting for `eblu.me`

View file

@ -5,5 +5,27 @@ set -euo pipefail
export MISE_TASK_OUTPUT=interleave export MISE_TASK_OUTPUT=interleave
# Update flake.lock via Dagger before deploying
echo "Updating nixos/ringtail/flake.lock..."
dagger call flake-lock --src=. --flake-path=nixos/ringtail \
export --path=nixos/ringtail/flake.lock
if ! git diff --quiet nixos/ringtail/flake.lock; then
git add nixos/ringtail/flake.lock
echo "flake.lock changed and staged. Commit, push, and re-run."
exit 1
fi
COMMIT=$(git rev-parse HEAD)
REMOTE_REF=$(git ls-remote origin "$(git rev-parse --abbrev-ref HEAD)" 2>/dev/null | awk '{print $1}')
if [[ "$REMOTE_REF" != "$COMMIT" ]]; then
echo "ERROR: Current commit $COMMIT is not pushed to forge."
echo "Push your changes first: git push"
exit 1
fi
echo "Deploying commit $COMMIT to ringtail..."
cd ansible cd ansible
ansible-playbook playbooks/ringtail.yml "$@" ansible-playbook playbooks/ringtail.yml -e "ringtail_commit=$COMMIT" "$@"

View file

@ -83,6 +83,11 @@ check_http "CV" "https://cv.ops.eblu.me/"
check_http "Ntfy" "https://ntfy.ops.eblu.me/v1/health" check_http "Ntfy" "https://ntfy.ops.eblu.me/v1/health"
check_http "Frigate" "https://nvr.ops.eblu.me/api/version" check_http "Frigate" "https://nvr.ops.eblu.me/api/version"
echo ""
echo "Ringtail (NixOS):"
check_service "ssh" "ssh -o ConnectTimeout=5 ringtail true"
check_service "tailscale" "ssh ringtail 'tailscale status --self --json' | jq -e '.Self.Online' > /dev/null"
echo "" echo ""
echo "Public services (via Fly.io):" echo "Public services (via Fly.io):"
check_http "Docs (public)" "https://docs.eblu.me/" check_http "Docs (public)" "https://docs.eblu.me/"

View file

@ -1,5 +1,9 @@
{ config, pkgs, ... }: { config, pkgs, lib, ... }:
let
# Libraries needed by mise-compiled runtimes (python-build, etc.)
buildDeps = with pkgs; [ zlib readline bzip2 xz libffi ncurses sqlite openssl ];
in
{ {
# Allow unfree packages (NVIDIA drivers, Steam) # Allow unfree packages (NVIDIA drivers, Steam)
nixpkgs.config.allowUnfree = true; nixpkgs.config.allowUnfree = true;
@ -8,6 +12,9 @@
boot.loader.systemd-boot.enable = true; boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true; boot.loader.efi.canTouchEfiVariables = true;
# No TPM module on this board
systemd.tpm2.enable = false;
# Networking # Networking
networking.hostName = "ringtail"; networking.hostName = "ringtail";
networking.networkmanager.enable = true; networking.networkmanager.enable = true;
@ -66,6 +73,23 @@
pulse.enable = true; pulse.enable = true;
}; };
# Bluetooth
hardware.bluetooth = {
enable = true;
powerOnBoot = true;
};
services.blueman.enable = true;
# Fish shell
programs.fish.enable = true;
# 1Password (modules handle CLI group/setgid and polkit for GUI integration)
programs._1password.enable = true;
programs._1password-gui = {
enable = true;
polkitPolicyOwners = [ "eblume" ];
};
# Steam # Steam
programs.steam = { programs.steam = {
enable = true; enable = true;
@ -90,7 +114,7 @@
# User account # User account
users.users.eblume = { users.users.eblume = {
isNormalUser = true; isNormalUser = true;
initialPassword = "changeme"; shell = pkgs.fish;
extraGroups = [ "wheel" "networkmanager" "video" ]; extraGroups = [ "wheel" "networkmanager" "video" ];
openssh.authorizedKeys.keys = [ openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILmh1SSCdDAyu3vkSQH7kAXEPDi8APyjo9JXDTjtha2j" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILmh1SSCdDAyu3vkSQH7kAXEPDi8APyjo9JXDTjtha2j"
@ -105,6 +129,244 @@
htop htop
curl curl
wget wget
chezmoi
neovim
eza
fd
fzf
zoxide
starship
atuin
bat
ripgrep
mise
gcc
gnumake
pkg-config
openssl
gnupg
unzip
fuzzel
pulseaudio
librewolf
];
# Allow running dynamically linked binaries (mise-installed runtimes, etc.)
programs.nix-ld.enable = true;
programs.nix-ld.libraries = buildDeps ++ [ pkgs.icu ];
# Compile-time flags for mise python-build and similar source builds
environment.sessionVariables = {
PKG_CONFIG_PATH = lib.makeSearchPath "lib/pkgconfig" (map lib.getDev buildDeps);
CFLAGS = lib.concatMapStringsSep " " (p: "-I${lib.getDev p}/include") buildDeps;
LDFLAGS = lib.concatMapStringsSep " " (p: "-L${lib.getLib p}/lib") buildDeps;
};
# Fonts
fonts.packages = with pkgs; [
nerd-fonts.victor-mono
];
# Home Manager (minimal — chezmoi owns dotfiles, this is ringtail-specific)
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.eblume = {
home.stateVersion = "25.11";
wayland.windowManager.sway = {
enable = true;
checkConfig = false;
config = {
terminal = "wezterm";
modifier = "Mod4";
fonts = {
names = [ "VictorMono Nerd Font" ];
size = 10.0;
};
bars = [{ command = "waybar"; }];
gaps = {
inner = 8;
outer = 4;
};
window = {
border = 2;
titlebar = false;
};
colors = {
focused = {
border = "#8aadf4";
background = "#24273a";
text = "#cad3f5";
indicator = "#c6a0f6";
childBorder = "#8aadf4";
};
focusedInactive = {
border = "#494d64";
background = "#1e2030";
text = "#a5adcb";
indicator = "#494d64";
childBorder = "#494d64";
};
unfocused = {
border = "#363a4f";
background = "#1e2030";
text = "#6e738d";
indicator = "#363a4f";
childBorder = "#363a4f";
};
urgent = {
border = "#ed8796";
background = "#24273a";
text = "#cad3f5";
indicator = "#ed8796";
childBorder = "#ed8796";
};
};
input = {
"*" = {
xkb_options = "ctrl:nocaps";
};
};
output = {
"DP-1" = {
mode = "2560x1440@165Hz";
adaptive_sync = "on";
bg = "~/.config/sway/wallpaper.jpg fill";
};
};
keybindings = let mod = "Mod4"; in {
"${mod}+Return" = "exec wezterm";
"${mod}+Shift+q" = "kill";
"${mod}+d" = "exec wmenu-run";
"${mod}+space" = "exec fuzzel";
"${mod}+Shift+c" = "reload";
"--locked XF86AudioMute" = "exec pactl set-sink-mute @DEFAULT_SINK@ toggle";
"--locked XF86AudioLowerVolume" = "exec pactl set-sink-volume @DEFAULT_SINK@ -5%";
"--locked XF86AudioRaiseVolume" = "exec pactl set-sink-volume @DEFAULT_SINK@ +5%";
"--locked XF86AudioMicMute" = "exec pactl set-source-mute @DEFAULT_SOURCE@ toggle";
};
startup = [
{ command = "1password"; }
{ command = "steam"; }
];
};
};
programs.fuzzel = {
enable = true;
settings = {
main = {
font = "VictorMono Nerd Font:size=14";
terminal = "wezterm";
width = 40;
horizontal-pad = 16;
vertical-pad = 8;
border-radius = 8;
border-width = 2;
};
colors = {
background = "24273add";
text = "cad3f5ff";
match = "8aadf4ff";
selection = "363a4fff";
selection-text = "cad3f5ff";
selection-match = "8aadf4ff";
border = "8aadf4ff";
};
};
};
programs.waybar = {
enable = true;
settings = [{
layer = "top";
position = "top";
height = 30;
modules-left = [ "sway/workspaces" "sway/mode" ];
modules-center = [ "sway/window" ];
modules-right = [ "pulseaudio" "bluetooth" "network" "clock" "tray" ];
tray = { spacing = 8; };
clock = { format = "{:%a %b %d %H:%M}"; };
network = {
interval = 2;
format-ethernet = "{bandwidthDownBits} down {bandwidthUpBits} up";
format-wifi = "{essid} {bandwidthDownBits} down {bandwidthUpBits} up";
format-disconnected = "disconnected";
};
pulseaudio = {
format = "{icon} {volume}%";
format-muted = " muted";
format-icons = {
headphone = "";
default = [ "" "" "" ];
};
};
}];
style = ''
* {
font-family: "VictorMono Nerd Font";
font-size: 13px;
border: none;
border-radius: 0;
min-height: 0;
}
window#waybar {
background-color: rgba(30, 32, 48, 0.9);
color: #cad3f5;
margin: 4px 4px 0 4px;
}
#workspaces button {
padding: 0 8px;
margin: 0 2px;
color: #6e738d;
background: transparent;
border-radius: 4px;
}
#workspaces button.focused {
color: #8aadf4;
background: #363a4f;
border-bottom: 2px solid #8aadf4;
}
#workspaces button.urgent {
color: #ed8796;
}
#window {
color: #a5adcb;
}
#bluetooth {
color: #8aadf4;
}
#bluetooth.off, #bluetooth.disabled {
color: #6e738d;
}
#clock, #network, #pulseaudio, #bluetooth, #tray {
padding: 0 12px;
margin: 4px 2px;
color: #cad3f5;
background: #363a4f;
border-radius: 4px;
}
#clock {
color: #8aadf4;
}
#pulseaudio {
color: #f5a97f;
}
#network {
color: #a6da95;
}
#network.disconnected {
color: #ed8796;
}
'';
};
};
# Ensure mounted drives are owned by eblume
systemd.tmpfiles.rules = [
"d /mnt/games 0755 eblume users -"
"d /mnt/storage1 0755 eblume users -"
"d /mnt/storage2 0755 eblume users -"
]; ];
# Enable nix flakes # Enable nix flakes

View file

@ -20,6 +20,27 @@
"type": "github" "type": "github"
} }
}, },
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1770260404,
"narHash": "sha256-3iVX1+7YUIt23hBx1WZsUllhbmP2EnXrV8tCRbLxHc8=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "0d782ee42c86b196acff08acfbf41bb7d13eed5b",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-25.11",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1771208521, "lastModified": 1771208521,
@ -39,6 +60,7 @@
"root": { "root": {
"inputs": { "inputs": {
"disko": "disko", "disko": "disko",
"home-manager": "home-manager",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
} }

View file

@ -1,5 +1,5 @@
{ {
description = "NixOS configuration for ringtail (gaming/compute workstation)"; description = "NixOS configuration for ringtail (service host & gaming PC)";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
@ -7,13 +7,18 @@
url = "github:nix-community/disko"; url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
home-manager = {
url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = { nixpkgs, disko, ... }: { outputs = { nixpkgs, disko, home-manager, ... }: {
nixosConfigurations.ringtail = nixpkgs.lib.nixosSystem { nixosConfigurations.ringtail = nixpkgs.lib.nixosSystem {
system = "x86_64-linux"; system = "x86_64-linux";
modules = [ modules = [
disko.nixosModules.disko disko.nixosModules.disko
home-manager.nixosModules.home-manager
./disk-config.nix ./disk-config.nix
./hardware-configuration.nix ./hardware-configuration.nix
./configuration.nix ./configuration.nix