diff --git a/.dagger/src/blumeops_ci/main.py b/.dagger/src/blumeops_ci/main.py index 5cd70e4..b14057a 100644 --- a/.dagger/src/blumeops_ci/main.py +++ b/.dagger/src/blumeops_ci/main.py @@ -1,6 +1,8 @@ import dagger from dagger import dag, function, object_type +NIX_IMAGE = "nixos/nix:2.33.3" + @object_type class BlumeopsCi: @@ -67,3 +69,26 @@ class BlumeopsCi: ) .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") + ) diff --git a/ansible/playbooks/ringtail.yml b/ansible/playbooks/ringtail.yml index 61911a1..f7e085a 100644 --- a/ansible/playbooks/ringtail.yml +++ b/ansible/playbooks/ringtail.yml @@ -8,7 +8,7 @@ ansible.builtin.git: repo: "https://forge.ops.eblu.me/eblume/blumeops.git" dest: /etc/blumeops - version: main + version: "{{ ringtail_commit | default('main') }}" force: true register: _repo @@ -16,7 +16,7 @@ ansible.builtin.command: cmd: nixos-rebuild switch --flake /etc/blumeops/nixos/ringtail#ringtail register: _rebuild - changed_when: "'activating the configuration' in _rebuild.stdout" + changed_when: "'activating the configuration' in _rebuild.stderr" when: _repo.changed - name: Verify tailscale is connected diff --git a/docs/changelog.d/feature-ringtail-nixos.infra.md b/docs/changelog.d/feature-ringtail-nixos.infra.md new file mode 100644 index 0000000..8c5ddf0 --- /dev/null +++ b/docs/changelog.d/feature-ringtail-nixos.infra.md @@ -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. diff --git a/docs/reference/infrastructure/hosts.md b/docs/reference/infrastructure/hosts.md index 08950b7..f8b07ff 100644 --- a/docs/reference/infrastructure/hosts.md +++ b/docs/reference/infrastructure/hosts.md @@ -1,6 +1,6 @@ --- title: Hosts -modified: 2026-02-10 +modified: 2026-02-18 tags: - 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]] | | **Gilbert** | MacBook Air M4, 2025 - Workstation | [[gilbert|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 | - | | **UniFi** | UniFi Express 7 - Home WiFi | [[unifi|Details]] | | **Dwarf** | iPad Air - Employer-provided, off tailnet | - | diff --git a/docs/reference/infrastructure/ringtail.md b/docs/reference/infrastructure/ringtail.md new file mode 100644 index 0000000..7906979 --- /dev/null +++ b/docs/reference/infrastructure/ringtail.md @@ -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 diff --git a/docs/reference/reference.md b/docs/reference/reference.md index 13a773c..e1cacc0 100644 --- a/docs/reference/reference.md +++ b/docs/reference/reference.md @@ -47,6 +47,7 @@ Host inventory and network configuration. - [[hosts|Hosts]] - Device inventory - [[indri]] - Primary server +- [[ringtail]] - Service host & gaming PC - [[gilbert]] - Development workstation - [[tailscale]] - ACLs, groups, tags - [[gandi]] - DNS hosting for `eblu.me` diff --git a/mise-tasks/provision-ringtail b/mise-tasks/provision-ringtail index cb5effe..a2a84cb 100755 --- a/mise-tasks/provision-ringtail +++ b/mise-tasks/provision-ringtail @@ -5,5 +5,27 @@ set -euo pipefail 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 -ansible-playbook playbooks/ringtail.yml "$@" +ansible-playbook playbooks/ringtail.yml -e "ringtail_commit=$COMMIT" "$@" diff --git a/mise-tasks/services-check b/mise-tasks/services-check index 020e177..31c8cc5 100755 --- a/mise-tasks/services-check +++ b/mise-tasks/services-check @@ -83,6 +83,11 @@ check_http "CV" "https://cv.ops.eblu.me/" check_http "Ntfy" "https://ntfy.ops.eblu.me/v1/health" 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 "Public services (via Fly.io):" check_http "Docs (public)" "https://docs.eblu.me/" diff --git a/nixos/ringtail/configuration.nix b/nixos/ringtail/configuration.nix index dd97cf5..9e5ecec 100644 --- a/nixos/ringtail/configuration.nix +++ b/nixos/ringtail/configuration.nix @@ -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) nixpkgs.config.allowUnfree = true; @@ -8,6 +12,9 @@ boot.loader.systemd-boot.enable = true; boot.loader.efi.canTouchEfiVariables = true; + # No TPM module on this board + systemd.tpm2.enable = false; + # Networking networking.hostName = "ringtail"; networking.networkmanager.enable = true; @@ -66,6 +73,23 @@ 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 programs.steam = { enable = true; @@ -90,7 +114,7 @@ # User account users.users.eblume = { isNormalUser = true; - initialPassword = "changeme"; + shell = pkgs.fish; extraGroups = [ "wheel" "networkmanager" "video" ]; openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILmh1SSCdDAyu3vkSQH7kAXEPDi8APyjo9JXDTjtha2j" @@ -105,6 +129,244 @@ htop curl 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 diff --git a/nixos/ringtail/flake.lock b/nixos/ringtail/flake.lock index d5ba048..2cd5e75 100644 --- a/nixos/ringtail/flake.lock +++ b/nixos/ringtail/flake.lock @@ -20,6 +20,27 @@ "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": { "locked": { "lastModified": 1771208521, @@ -39,6 +60,7 @@ "root": { "inputs": { "disko": "disko", + "home-manager": "home-manager", "nixpkgs": "nixpkgs" } } diff --git a/nixos/ringtail/flake.nix b/nixos/ringtail/flake.nix index 8bfac2f..70a1d73 100644 --- a/nixos/ringtail/flake.nix +++ b/nixos/ringtail/flake.nix @@ -1,5 +1,5 @@ { - description = "NixOS configuration for ringtail (gaming/compute workstation)"; + description = "NixOS configuration for ringtail (service host & gaming PC)"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; @@ -7,13 +7,18 @@ url = "github:nix-community/disko"; 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 { system = "x86_64-linux"; modules = [ disko.nixosModules.disko + home-manager.nixosModules.home-manager ./disk-config.nix ./hardware-configuration.nix ./configuration.nix