Making it Production Ready: Adding Services and Hardening: Part 4 of "Building a Resilient Home Server" Series"

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

Where We Left Off

In Part 3, we got the custom NixOS ISO working and deployed to real hardware. AdGuard Home was running, the server was stable, and I had a reproducible installation process. Victory, right?

Well, sort of. The server was working, but it was still pretty bare-bones. No web server for future services. SSH was functional but not hardened. No secure remote access. And I'd lost Home Manager somewhere along the way during earlier troubleshooting.

Time to level up....or if you grew up with Digimon....Time to Digivolve! 


The Expansion Plan

Here's what needed to happen:

  1. Nginx - A proper web server foundation for future services
  2. SSH Hardening - Fail2Ban, key-only authentication, the works
  3. Home Manager - Restore user environment management (properly this time)
  4. Tailscale - Secure remote access without punching holes in my firewall

Each of these meant updating the configuration, testing in the VM, and then updating the ISO to include everything.


Adding Nginx

Nginx was straightforward. I created modules/services.nix to keep things organized:

Nix

{ config, pkgs, ... }:

{
services.nginx = {
enable = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
recommendedOptimisation = true;
recommendedGzipSettings = true;
};

networking.firewall.allowedTCPPorts = [ 80 443 ];
}

Simple. Clean. Ready for future services like Nextcloud or whatever else I decide to throw at it.


SSH Hardening

SSH was already working, but "working" and "secure" are different things. I updated modules/networking.nix:

Nix

services.openssh = {
enable = true;
settings = {
PermitRootLogin = "no";
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
};
ports = [ 22 ]; # Change this in production
};

# Add Fail2Ban
services.fail2ban = {
enable = true;
maxretry = 3;
bantime = "1h";
};

Key-only authentication. No root login. Fail2Ban watching for brute force attempts. Much better.


Restoring Home Manager

Home Manager had been a casualty of earlier troubleshooting. Time to bring it back properly. I created home/ppb1701.nix:

Nix

{ config, pkgs, ... }:

{
home.username = "ppb1701";
home.homeDirectory = "/home/ppb1701";
home.stateVersion = "25.05";

programs.bash = {
enable = true;
shellAliases = {
ll = "ls -la";
update = "sudo nixos-rebuild switch";
};
};

programs.starship = {
enable = true;
settings = {
# Custom prompt configuration
};
};
}

Then imported it in the main configuration:

Nix

imports = [
<home-manager/nixos>
./home/ppb1701.nix
];


Adding Tailscale

Tailscale was the game-changer. Secure remote access without exposing SSH to the internet? Yes please.

Added to modules/services.nix:

Nix

services.tailscale = {
enable = true;
useRoutingFeatures = "server";
};

After a nixos-rebuild switch, I ran:

Bash

sudo tailscale up --accept-routes

Now I can SSH into my server from anywhere using its Tailscale IP. No port forwarding. No VPN complexity. Just works.


The ISO Update Challenge

Here's where things got interesting. I had all these new services working on the server, but the ISO still had the old configuration. I needed to update three things:

  1. The directory structure had evolved (modules/home/private/)
  2. iso-config.nix needed to include all the new files
  3. install-nixos.sh needed some improvements

When I used environment.etc to copy files to the ISO, NixOS creates symlinks to the Nix store. That's fine for the ISO itself, but when the installer tried to copy those "files" to the target system, it was copying symlinks, not actual files.

The solution? The -L flag in the cp command:

Bash

# In install-nixos.sh
cp -rL /etc/nixos/modules/* /mnt/etc/nixos/modules/

The -L flag tells cp to follow symlinks and copy the actual file content. Problem solved.

Updated iso-config.nix

I had to explicitly list every configuration file:

Nix

# Copy individual configuration files
environment.etc."nixos/configuration.nix".source = ./configuration.nix;
environment.etc."nixos/configuration-uefi.nix".source = ./configuration-uefi.nix;
environment.etc."nixos/configuration-bios.nix".source = ./configuration-bios.nix;

# Copy modules
environment.etc."nixos/modules/boot-bios.nix".source = ./modules/boot-bios.nix;
environment.etc."nixos/modules/boot-uefi.nix".source = ./modules/boot-uefi.nix;
environment.etc."nixos/modules/networking.nix".source = ./modules/networking.nix;
environment.etc."nixos/modules/services.nix".source = ./modules/services.nix;

# Copy home configuration
environment.etc."nixos/home/ppb1701.nix".source = ./home/ppb1701.nix;

# Copy private files
environment.etc."nixos/private/ssh-keys.nix".source = ./private/ssh-keys.nix;
environment.etc."nixos/private/syncthing-devices.nix".source = ./private/syncthing-devices.nix;

Tedious? Yes. But it works reliably.

Improved install-nixos.sh

I added an exit option to the boot mode selection:

Bash

echo "Select bootloader type:"
echo "1) UEFI (modern systems, systemd-boot)"
echo "2) BIOS/Legacy (older systems, GRUB)"
echo "3) Exit to live environment"
echo ""
read -p "Enter choice (1, 2, or 3): " BOOT_CHOICE

And updated all the copy commands to use -L:

Bash

cp -rL /etc/nixos/modules/* /mnt/etc/nixos/modules/
cp -rL /etc/nixos/home/* /mnt/etc/nixos/home/
cp -rL /etc/nixos/private/* /mnt/etc/nixos/private/


Testing the Updated ISO

Built the new ISO:

Bash

sudo ./build-iso.sh

Loaded it into VirtualBox. Ran the installer. Watched it deploy:

  • AdGuard Home
  • Nginx
  • Tailscale
  • Hardened SSH
  • Home Manager configuration

Everything worked. First try. (Okay, maybe not first try, but close enough... eventually overcame the symlinks.)


The Result

One ISO. One installation process. Everything deployed automatically:

✅ AdGuard Home - Network-wide ad blocking
✅ Nginx - Web server ready for future services
✅ Tailscale - Secure remote access
✅ Hardened SSH - Key-only auth + Fail2Ban
✅ Home Manager - User environment management
✅ Fully Declarative - Everything in Git

The "resilient home server" is starting to live up to its name.


What's Next

The server is running. The ISO is solid. But there's more to do:

  • Monitoring - Prometheus? Grafana? Something to know when things go wrong
  • Nextcloud - Self-hosted cloud storage
  • AdGuard Filters - Fine-tuning the blocklists

But honestly? I'm going to take a break from this project for a bit. I've got other things I want to work on, and this server is stable enough to run while I do. I'll circle back when I'm ready to add the next layer.

That's the beauty of NixOS - the server will be exactly where I left it when I come back.

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.