I had an old laptop collecting dust, so I turned it into a quiet, low-power home server. The stack is simple and resilient:

  • NixOS for reproducible system config
  • Pi-hole for network-wide ad and tracker blocking
  • Unbound as a local recursive DNS resolver

This setup gives you better privacy, fewer third-party DNS dependencies, and a server you can rebuild from config.

Why an old laptop works well

An old laptop is a solid starter server because it already includes:

  • Built-in UPS behavior (battery)
  • Low power usage
  • Quiet operation
  • Enough CPU and RAM for DNS, reverse proxy, backups, and small apps

For a home lab, that is usually more valuable than raw performance.

NixOS base setup

After installing NixOS, I keep almost everything in /etc/nixos/configuration.nix (and split into modules later).

Minimal baseline:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{ config, pkgs, ... }:

{
networking.hostName = "homelab";
time.timeZone = "Europe/Belgrade";

services.openssh.enable = true;
users.users.nemanja = {
isNormalUser = true;
extraGroups = [ "wheel" ];
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAA...your-key"
];
};

security.sudo.wheelNeedsPassword = false;

networking.firewall = {
enable = true;
allowedTCPPorts = [ 22 53 80 443 ];
allowedUDPPorts = [ 53 ];
};

system.stateVersion = "25.05";
}

Then apply:

1
sudo nixos-rebuild switch

Pi-hole + Unbound architecture

DNS flow:

  1. Devices send DNS queries to Pi-hole.
  2. Pi-hole filters known ads/trackers.
  3. Allowed queries are forwarded to local Unbound.
  4. Unbound resolves recursively from root DNS servers.

This avoids sending all your DNS traffic to a public resolver.

Containerized services with Podman

I run Pi-hole and Unbound as containers with Podman (rootless where practical). A compact compose.yml example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
services:
pihole:
image: docker.io/pihole/pihole:latest
container_name: pihole
ports:
- "53:53/tcp"
- "53:53/udp"
- "8080:80/tcp"
environment:
TZ: "Europe/Belgrade"
FTLCONF_webserver_api_password: "change-me"
PIHOLE_DNS_: "unbound#5335"
volumes:
- ./pihole:/etc/pihole
- ./dnsmasq.d:/etc/dnsmasq.d
restart: unless-stopped

unbound:
image: docker.io/mvance/unbound:latest
container_name: unbound
ports:
- "5335:53/tcp"
- "5335:53/udp"
restart: unless-stopped

Bring it up:

1
podman compose up -d

Hardening and reliability checklist

  • Set router DHCP to advertise only Pi-hole as DNS.
  • Keep server on a static LAN IP.
  • Back up Pi-hole config and your NixOS config repo.
  • Enable automatic NixOS updates only after testing on a safe schedule.
  • Pin container images when you want strict reproducibility.

What improved immediately

  • Faster perceived browsing on many sites
  • Cleaner mobile apps due to DNS-level blocking
  • Better observability of noisy clients in the network
  • Confidence that I can rebuild the server from code

Final note

If you are starting self-hosting, this is a high-value first project. You get practical Linux admin experience, network visibility, and a privacy upgrade without buying new hardware.