Add NixOS configuration for ringtail workstation (#207)

## Summary
- NixOS flake for ringtail (gaming/compute workstation, RTX 4080) in `nixos/ringtail/`
- Declarative disk partitioning via disko (GPT, 512M EFI + ext4 root on NVMe)
- NVIDIA proprietary drivers, sway/Wayland desktop, greetd, PipeWire, Steam
- Tailscale integration for tailnet connectivity
- Ansible playbook + `mise run provision-ringtail` for ongoing management
- Pulumi auth key (`tag:homelab`, `tag:blumeops`) for tailnet bootstrap

## Deployment Order
1. **Merge PR**
2. `pulumi up` in tailscale stack → creates auth key
3. Retrieve auth key: `pulumi stack output ringtail_authkey --show-secrets`
4. On ringtail NixOS installer:
   - `nix run github:nix-community/disko -- --mode disko /tmp/disk-config.nix` (or from cloned repo)
   - `nixos-install --flake github:eblume/blumeops?dir=nixos/ringtail#ringtail`
5. Reboot, `tailscale up --auth-key=<key>`
6. Verify: `tailscale status`, SSH from gilbert

## Test plan
- [ ] Review NixOS configuration for completeness
- [ ] Verify disko partition layout matches ringtail hardware
- [ ] Run `pulumi preview` for tailscale stack
- [ ] Install NixOS on ringtail
- [ ] Confirm tailscale connectivity
- [ ] Confirm sway desktop works
- [ ] Test `mise run provision-ringtail` for ongoing management

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/207
This commit is contained in:
Erich Blume 2026-02-18 08:24:25 -08:00
commit b9d813cde1
9 changed files with 281 additions and 1 deletions

View file

@ -5,6 +5,9 @@ all:
hosts: hosts:
indri: indri:
ansible_host: indri ansible_host: indri
ringtail:
ansible_host: ringtail
ansible_user: eblume
workstations: workstations:
hosts: hosts:
gilbert: gilbert:

View file

@ -0,0 +1,25 @@
---
- name: Configure ringtail (NixOS)
hosts: ringtail
become: true
tasks:
- name: Ensure blumeops repo is present
ansible.builtin.git:
repo: "https://forge.ops.eblu.me/eblume/blumeops.git"
dest: /etc/blumeops
version: main
register: _repo
- name: Rebuild NixOS
ansible.builtin.command:
cmd: nixos-rebuild switch --flake /etc/blumeops/nixos/ringtail#ringtail
register: _rebuild
changed_when: "'activating the configuration' in _rebuild.stdout"
when: _repo.changed
- name: Verify tailscale is connected
ansible.builtin.command: tailscale status --self --json
register: _ts_status
changed_when: false
failed_when: "'Running' not in _ts_status.stdout"

View file

@ -0,0 +1 @@
Add NixOS configuration for ringtail (gaming/compute workstation with RTX 4080). Includes declarative disk partitioning via disko, NVIDIA drivers, sway/Wayland desktop, Steam, Tailscale, and Ansible-driven provisioning.

9
mise-tasks/provision-ringtail Executable file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env bash
#MISE description="Run ansible playbook to provision ringtail (NixOS)"
set -euo pipefail
export MISE_TASK_OUTPUT=interleave
cd ansible
ansible-playbook playbooks/ringtail.yml "$@"

View file

@ -0,0 +1,104 @@
{ config, pkgs, ... }:
{
# Bootloader
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
# Networking
networking.hostName = "ringtail";
networking.networkmanager.enable = true;
# Time zone
time.timeZone = "America/Los_Angeles";
# Locale
i18n.defaultLocale = "en_US.UTF-8";
# NVIDIA proprietary drivers
hardware.graphics.enable = true;
services.xserver.videoDrivers = [ "nvidia" ];
hardware.nvidia = {
modesetting.enable = true;
open = false; # Use proprietary driver for RTX 4080
nvidiaSettings = true;
package = config.boot.kernelPackages.nvidiaPackages.stable;
};
# Wayland / Sway
programs.sway = {
enable = true;
wrapperFeatures.gtk = true;
extraPackages = with pkgs; [
swaylock
swayidle
wezterm # terminal
wmenu # app launcher
mako # notifications
grim # screenshots
slurp # region selection
];
};
security.polkit.enable = true;
# Enable greetd as display manager for sway
services.greetd = {
enable = true;
settings = {
default_session = {
command = "${pkgs.greetd.tuigreet}/bin/tuigreet --time --cmd sway";
user = "greeter";
};
};
};
# PipeWire for audio
services.pipewire = {
enable = true;
alsa.enable = true;
pulse.enable = true;
};
# Steam
programs.steam = {
enable = true;
dedicatedServer.openFirewall = true;
};
# Tailscale
services.tailscale.enable = true;
# SSH
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
};
};
# User account
users.users.eblume = {
isNormalUser = true;
initialPassword = "changeme";
extraGroups = [ "wheel" "networkmanager" "video" ];
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILmh1SSCdDAyu3vkSQH7kAXEPDi8APyjo9JXDTjtha2j"
];
};
# System packages
environment.systemPackages = with pkgs; [
git
vim
htop
curl
wget
];
# Enable nix flakes
nix.settings.experimental-features = [ "nix-command" "flakes" ];
# NixOS release
system.stateVersion = "25.11";
}

View file

@ -0,0 +1,84 @@
{
disko.devices = {
disk = {
nvme = {
type = "disk";
device = "/dev/nvme0n1";
content = {
type = "gpt";
partitions = {
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
games = {
type = "disk";
device = "/dev/sda";
content = {
type = "gpt";
partitions = {
games = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/mnt/games";
};
};
};
};
};
storage1 = {
type = "disk";
device = "/dev/sdb";
content = {
type = "gpt";
partitions = {
storage1 = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/mnt/storage1";
};
};
};
};
};
storage2 = {
type = "disk";
device = "/dev/sdc";
content = {
type = "gpt";
partitions = {
storage2 = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/mnt/storage2";
};
};
};
};
};
};
};
}

23
nixos/ringtail/flake.nix Normal file
View file

@ -0,0 +1,23 @@
{
description = "NixOS configuration for ringtail (gaming/compute workstation)";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { nixpkgs, disko, ... }: {
nixosConfigurations.ringtail = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
disko.nixosModules.disko
./disk-config.nix
./hardware-configuration.nix
./configuration.nix
];
};
};
}

View file

@ -0,0 +1,18 @@
# Do not modify this file! It was generated by 'nixos-generate-config'
# and may be overwritten by future invocations. Please make changes
# to configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usb_storage" "usbhid" "sd_mod" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-amd" ];
boot.extraModulePackages = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

View file

@ -5,7 +5,7 @@ This program manages:
- Device tags for infrastructure classification - Device tags for infrastructure classification
Devices are tagged based on their role: Devices are tagged based on their role:
- tag:homelab - Server infrastructure (indri) - tag:homelab - Server infrastructure (indri, ringtail)
- tag:workstation - Development machines that can manage homelab (gilbert) - tag:workstation - Development machines that can manage homelab (gilbert)
- tag:nas - Network-attached storage (sifaka) - tag:nas - Network-attached storage (sifaka)
- tag:blumeops - Resources managed by this IaC - tag:blumeops - Resources managed by this IaC
@ -82,10 +82,23 @@ flyio_key = tailscale.TailnetKey(
expiry=7776000, # 90 days expiry=7776000, # 90 days
) )
# Auth key for ringtail (gaming/compute workstation, NixOS)
# Used during bootstrap: `tailscale up --auth-key=<key>`
# Once ringtail is on the tailnet, add DeviceTags resource for ongoing management.
ringtail_key = tailscale.TailnetKey(
"ringtail-key",
reusable=False,
ephemeral=False,
preauthorized=True,
tags=["tag:homelab", "tag:blumeops"],
expiry=86400, # 24 hours - single use for bootstrap
)
# ============== Exports ============== # ============== Exports ==============
pulumi.export("acl_id", acl.id) pulumi.export("acl_id", acl.id)
pulumi.export("policy_hash", policy_hash) pulumi.export("policy_hash", policy_hash)
pulumi.export("flyio_authkey", flyio_key.key) pulumi.export("flyio_authkey", flyio_key.key)
pulumi.export("ringtail_authkey", ringtail_key.key)
pulumi.export("indri_device_id", indri.node_id) pulumi.export("indri_device_id", indri.node_id)
pulumi.export("indri_tags", indri_tags.tags) pulumi.export("indri_tags", indri_tags.tags)