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:
- Nginx - A proper web server foundation for future services
- SSH Hardening - Fail2Ban, key-only authentication, the works
- Home Manager - Restore user environment management (properly this time)
- 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:
- The directory structure had evolved (
modules/,home/,private/) -
iso-config.nixneeded to include all the new files -
install-nixos.shneeded some improvements
A New Problem: Symlinks
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.