From VM To Reality - When Things Break: Part 3 of "Building a Resilient Home Server" Series

*Part 3 of "Building a Resilient Home Server" series*

The Great Migration (and So Many Stairs)

At the end of Part 2, I had a working NixOS VM with AdGuard Home, SSH, and basic Zsh configuration. The VM was my playground—safe to break, easy to restore. But the real goal was getting this onto actual hardware.

I had an idea: build a custom NixOS ISO with my entire configuration baked in. Boot it, install it, done. No manual configuration copying, no forgetting steps. Everything declarative, everything reproducible.

Sounds simple, right?

The Custom ISO Attempt

Building a custom NixOS ISO requires creating an ISO configuration that includes your system configuration. I started researching the NixOS manual and various blog posts about custom ISOs.

The basic idea was to create a configuration that would:

  1. Boot from USB
  2. Include my user account with a password
  3. Have all my configurations ready to deploy

I set up the ISO configuration and built it. The build process took a while, but eventually I had a custom ISO file.

Burned it to a USB drive and headed to the server room with just the drive in hand.

The Password That Wasn't

Booted the USB on the real hardware. The ISO loaded. I tried to log in.

No password prompt. Just... nothing. I couldn't log in at all.

Back upstairs to check the configuration. I'd assumed the password from my VM would just... port over somehow. That the user account would work the same way.

Nope.

After digging through documentation, I discovered I needed to explicitly set initialPassword in the configuration for fresh installs. And it had to be plaintext in the config file—not ideal for security, but fine for a fresh install that would be secured immediately after first boot.

Oh, and as a side note: I do realize the more "Nix way" is using hashedPassword. I opted for not doing that for two reasons: 1) the initialPassword gives anyone the option to do either really when they secure it, and 2) in my case my repo is public... so hashed or not, I don't want a password for all the world to see and poke at.

Fixed the config, rebuilt the ISO, burned it to the drive again, back downstairs.

The UEFI Surprise

Booted the new USB. Got an error. The machine wouldn't boot the ISO at all.

After some head-scratching and research, I realized the problem: my VM was using BIOS/GRUB, but the real server hardware wanted UEFI/systemd-boot.

I'd built an ISO for the wrong boot mode.

Now I needed two configurations:

  • configuration-bios.nix for the VM (GRUB)
  • configuration-uefi.nix for real hardware (systemd-boot)

The configurations were identical except for the bootloader section:

BIOS/GRUB:

Nix

boot.loader.grub = {
enable = true;
device = "/dev/sda";
};

UEFI/systemd-boot:

Nix

boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;

Rebuilt the ISO with the UEFI configuration. Burned it. Back downstairs.

The Stair Trip Saga

PRO TIP: Don't do server setup in the middle of a three-week-long paint project where you're living on a different floor than the physical machine.

The ISO booted. I installed NixOS. Rebooted. Time to SSH in and finish the setup.

SSH didn't work.

Walked downstairs to check. Network was connected. IP address looked right. Walked back upstairs. Tried SSH again. Connection refused.

Walked back downstairs. Checked the firewall configuration. Looked fine. Walked back upstairs. Still couldn't connect.

Walked back downstairs. Checked SSH service status. It was running. Walked back upstairs. Still nothing.

This went on for... a while. Each trip revealed a new tiny issue:

  • Wrong IP address in my SSH config
  • Firewall rule not quite right
  • SSH listening on the wrong interface
  • Network configuration not persisting after reboot

Each fix required editing the configuration, rebuilding, rebooting, and testing. Each test required walking back upstairs to my desk.

I lost count of how many trips I made. My Fitbit was thrilled. I was not.

Eventually, after what felt like the hundredth trip, SSH worked. I could access the server from my desk. No more stairs.

The Desktop Environment Incident

With SSH working, I started refining the configuration. Adding packages, tweaking settings, making it feel like home.

At some point during this process, I accidentally removed or commented out the desktop environment configuration. I don't remember exactly what I did—probably got overzealous with cleanup while testing something.

Rebuilt. Rebooted. No desktop.

Just a black screen with a cursor.

Fortunately, I could still SSH in. I checked the configuration and realized what I'd done. The LXQt desktop environment section was gone.

This is where Git saved me. I'd been committing every change to my repository, so I could see exactly what I'd removed. Restored the desktop configuration:

Nix

services.xserver = {
enable = true;
displayManager.lightdm.enable = true;
desktopManager.lxqt.enable = true;
};

Rebuilt, rebooted, and the desktop came back.

Lesson reinforced: Commit everything to Git. When (not if) you break something, you can see exactly what changed.

The Starship Weekend

By this point, the server was stable and running. AdGuard Home was blocking ads, Syncthing was syncing files, and SSH was working reliably.

But my shell prompt was still boring. Basic Zsh with a plain prompt. Functional, but not inspiring.

Someone mentioned Starship in a discussion—described it as a fast, beautiful, cross-shell prompt written in Rust. I'd never seen it before, but the description was intriguing enough to try.

Added Starship to my configuration:

Nix

programs.starship.enable = true;

Rebuilt. The default Starship prompt appeared.

I absolutely loved it.

Already miles better than plain Zsh. Git branches were visible with a little plant emoji (🌱), command durations showed up, and everything was color-coded and informative. This was what a prompt should be.

Then I discovered you could customize it further. A lot further. And it accepted hex color codes.

That's when I went full Capy_UI. I spent the weekend diving into Starship configuration, lifting the color palette from Mastodon's Capy_UI theme. I also switched to Nerd Fonts—specifically JetBrains Nerd Font—to get proper icon support and better visual consistency.

Nix

programs.starship = {
enable = true;
settings = {
add_newline = false;

format = lib.concatStrings [
"$username"
"$hostname"
"$directory"
"$git_branch"
"$git_status"
"$cmd_duration"
"$line_break"
"$character"
];

character = {
success_symbol = "[❯](bold electric-blue)";
error_symbol = "[❯](bold red)";
};

git_branch = {
symbol = "🌱 ";
format = "[$symbol$branch]($style) ";
style = "bold electric-blue";
};

# ... more configuration with Capy_UI hex codes
};
};

The result was gorgeous. Everything was perfectly color-coded in that beautiful Capy_UI palette, with JetBrains Nerd Font rendering all the icons crisply. I deployed the same configuration to my Mac (via Homebrew) and Windows (via winget install starship).

One configuration, three platforms. That's the power of Nix... and cross-platform tools like Starship.

AdGuard Filter Tuning

With the server running smoothly and the prompt looking good, I turned my attention back to AdGuard Home.

The default filter lists were blocking ads, but I wanted better coverage for streaming services. I added custom rules for Apple TV apps that were showing ads:

Plaintext

||ads.paramount.com^
||ads.historychannel.com^

Filter tuning is an ongoing process. Add a rule, test it, see if it breaks something, adjust. I had to disable my browser's ad blocker to properly test whether AdGuard Home was doing its job.

What I Learned

Custom ISOs are powerful but tricky. Getting the boot mode right (BIOS vs UEFI) is critical. Build for the wrong mode and you'll waste time troubleshooting.

Password configuration matters. Use initialPassword for fresh installs, then change it immediately after first boot.

Physical access debugging is painful. Get SSH working as early as possible. Every trip to the server room is time wasted.

Git is essential. When you accidentally nuke your desktop environment (and you will), Git saves you.

Starship is worth the time. A good prompt makes the terminal feel like home. Once you customize it, you'll want it everywhere.

Filter tuning takes patience. AdGuard Home works great, but getting the filters just right requires testing and iteration.

The Server Was Ready... Almost

At this point, I had a working NixOS server running in production:

  • AdGuard Home blocking ads across the network
  • Syncthing syncing files between devices
  • SSH access with password authentication
  • Beautiful Starship prompt with my Capy_UI theme
  • All configurations in Git

But there were still improvements to make:

  • SSH was still using password authentication (not secure)
  • Starship and shell configs were system-level (should be user-level)
  • Home Manager had disappeared during the RustDesk saga and needed to be properly integrated

Those improvements would come in the following days...


To Be Continued...

In Part 4, I'll cover hardening SSH security with key-based authentication, properly integrating Home Manager for user-level configuration management, and the ongoing adventures of maintaining a NixOS home server.

Want to follow along or see the configs? Check out my NixOS configuration Main Server (nixos): Codeberg
Second Server (nixos2): Codeberg
ISO can be gotten here.

← Back to Series
                    
What do you think about NixOS for a homeserver? I'd love to hear your thoughts—find me on Mastodon at @ppb1701@ppb.social and let's talk NixOS or Linux in general.