< psritej.com / blog />

The Ultimate Raspberry Pi 5 Homelab Guide: DNS, NAS, and Observability

Sritej Panchumarthi · Published: May 5, 2026 · Updated: July 7, 2026 · 45 min read · #Homelab #Networking #RaspberryPi #SelfHosted

Key takeaway: A Raspberry Pi homelab becomes useful when it is treated like real infrastructure: static addressing, monitored services, backed-up state, reproducible configuration, and a clear recovery path when the boot drive or network fails. The Pi is small; the engineering discipline shouldn't be.

The cloud is great, but there is something visceral about owning your own metal — and pedagogically, nothing teaches networking, DNS, storage, and operations like being the person your household blames when the internet "breaks." With the Raspberry Pi 5's PCIe lane, we can finally build a serious, high-performance homelab that fits in the palm of your hand.

The mission: build a "set and forget" network appliance that handles:
1. Network-wide ad blocking (AdGuard Home)
2. Recursive DNS (Unbound) for privacy sovereignty
3. NAS storage (Samba) saturating Gigabit Ethernet
4. Observability (Netdata) so problems announce themselves
All with a read-only root overlay so power loss can never corrupt the boot medium.

1. L3 Network Architecture

Before touching hardware, understand the system you're building. The L3 diagram below shows every component, port, and traffic flow — including the two paths that matter most: the DNS resolution chain (every device → AdGuard → Unbound → root servers) and the failure path when the Pi is down.

Fig 1. L3 Component Architecture — Home Network with Pi 5 Edge Appliance
                            INTERNET
                               │
                     ┌─────────┴─────────┐
                     │  ISP Router       │  DHCP scope: .100–.250
                     │  192.168.1.1      │  DNS handed to clients:
                     │                   │   primary  → 192.168.1.5
                     │                   │   fallback → 1.1.1.1 ◄── the
                     └─────────┬─────────┘   "Pi is down" escape hatch
                               │ Cat6, 1 GbE
               ┌───────────────┼──────────────────┐
               ▼               ▼                  ▼
        ┌────────────┐  ┌────────────┐   ┌──────────────────────────┐
        │ Clients    │  │ IoT VLAN   │   │  RASPBERRY PI 5 (16 GB)  │
        │ laptops    │  │ (isolated, │   │  192.168.1.5  (static)   │
        │ phones     │  │  DNS-only  │   │  Ubuntu 24.04 + overlay  │
        │ consoles   │  │  egress)   │   │  root (read-only /)      │
        └─────┬──────┘  └─────┬──────┘   │                          │
              │ :53           │ :53      │  ┌────────────────────┐  │
              └───────────────┴─────────►│  │ AdGuard Home       │  │
                                         │  │ :53 dns  :3000 ui  │  │
   DNS RESOLUTION CHAIN                  │  │ filtering, per-    │  │
   client → AdGuard (filter/cache)       │  │ client stats, DoH  │  │
          → Unbound 127.0.0.1:5335       │  └─────────┬──────────┘  │
          → root servers (recursive,     │            │ upstream    │
            DNSSEC-validated)            │            ▼ :5335       │
   ~25 ms first lookup, <1 ms cached     │  ┌────────────────────┐  │
                                         │  │ Unbound (recursive)│  │
              ┌──────────────────────────┤  │ :5335 · DNSSEC     │  │
              │ SMB :445                 │  │ · qname-minimise   │  │
              ▼                          │  └────────────────────┘  │
        ┌────────────┐                   │  ┌────────────────────┐  │
        │ Workstation│◄── ~115 MB/s ────►│  │ Samba NAS :445     │  │
        │ backups,   │    (wire speed)   │  │ /mnt/data/share    │  │
        │ media      │                   │  └────────────────────┘  │
        └────────────┘                   │  ┌────────────────────┐  │
                                         │  │ Netdata :19999     │  │
   STORAGE                               │  │ cpu·temp·SMART·dns │  │
   NVMe 1TB via PCIe 2.0 x1              │  └────────────────────┘  │
   (~420 MB/s raw, ext4)                 │  Docker Compose manages  │
   /mnt/data ── containers state,        │  all services            │
                share, restic cache      └─────────────┬────────────┘
                                                       │ nightly
                                     ┌─────────────────┼───────────┐
                                     ▼                 ▼           │
                              USB HDD (local     AWS S3 Deep       │
                              restic repo)       Archive (restic,  │
                                                 encrypted) ◄──────┘
   3-2-1: live NVMe + local HDD + off-site S3 · restore drill monthly

2. Hardware Bill of Materials (BOM)

Don't just buy the board. Performance and reliability live in the accessories:

ComponentRecommendationWhy it matters
Raspberry Pi 58 GB minimum, 16 GB idealRAM is DNS cache, page cache for Samba, and container headroom
NVMe base/HATPimoroni NVMe Base, Pineberry HatDriveExposes the PCIe 2.0 x1 lane; SD cards are too slow and too fragile for a NAS
NVMe SSD1 TB+, known controller (WD SN580, Crucial P3)IOPS, endurance, and SMART telemetry the SD card can't offer
Power supplyOfficial 27 W USB-C PDThe Pi 5 + NVMe browns out on phone chargers; undervoltage corrupts writes
CoolingActive Cooler (official)Sustained NAS transfers throttle a passive Pi; the fan holds ~60 °C under load
EthernetCat6 to the routerA DNS/NAS server on Wi-Fi is a self-inflicted outage generator
Spare boot media32 GB SD card with known-good imageYour §8.3 recovery drill depends on it existing before the failure

Total spend lands around $180–230 — and it will idle at 4–6 W, roughly $7/year in electricity.

3. Operating System, Network, and Storage Setup

The system runs Ubuntu Server 24.04 LTS (64-bit). Three foundations before any service is installed: deterministic addressing, durable storage, and a corruption-proof root.

3.1 Static Addressing (Netplan)

A DNS server that changes its own IP takes the whole network down with it. Static, always:

# /etc/netplan/01-static.yaml
network:
  version: 2
  ethernets:
    eth0:
      dhcp4: false
      addresses:
        - 192.168.1.5/24
      routes:
        - to: default
          via: 192.168.1.1
      nameservers:
        addresses: [1.1.1.1, 8.8.8.8]   # temporary — until Unbound is live
sudo netplan apply
ip addr show eth0        # verify: inet 192.168.1.5/24

3.2 Storage Provisioning (NVMe, ext4, fstab by UUID)

sudo mkfs.ext4 -L data /dev/nvme0n1
sudo mkdir -p /mnt/data
# UUID, not device name — device enumeration can change across boots
echo "UUID=$(sudo blkid -s UUID -o value /dev/nvme0n1) /mnt/data ext4 defaults,noatime 0 2" \
  | sudo tee -a /etc/fstab
sudo mount -a && df -h /mnt/data

noatime eliminates a write per file-read — meaningful on flash media serving thousands of small DNS-state and container files.

3.3 Read-Only Root Overlay — the Corruption Killer

sudo apt install overlayroot
echo 'overlayroot="tmpfs:swap=1,recurse=0"' | sudo tee -a /etc/overlayroot.conf
# High-write paths must live on the NVMe (persistent), not the overlay:
#   Docker root → /mnt/data/docker   (daemon.json: {"data-root": "/mnt/data/docker"})
#   compose project + volumes → /mnt/data/stack
sudo reboot
# After reboot: mount | grep overlay  → root is now tmpfs-overlaid.
# To make deliberate system changes:  sudo overlayroot-chroot  → apt upgrade → reboot

With the overlay active, a power cut mid-write can no longer corrupt the root filesystem — the root is immutable and every boot is a known-good boot. Configuration changes now require a deliberate overlayroot-chroot, which for an appliance is exactly the right friction.

4. Service Orchestration (Docker Compose)

Services are containerized and orchestrated via Docker Compose — isolation, reproducibility, and a single file that is the documentation of what runs on this box:

# /mnt/data/stack/docker-compose.yml
services:
  # --- Core DNS & ad blocking ---
  adguard:
    image: adguard/adguardhome
    container_name: adguard
    network_mode: host          # needs :53 on the host for LAN-wide DNS
    restart: unless-stopped
    volumes:
      - /mnt/data/stack/adguard/work:/opt/adguardhome/work
      - /mnt/data/stack/adguard/conf:/opt/adguardhome/conf

  # --- Recursive resolver ---
  unbound:
    image: mvance/unbound:latest
    container_name: unbound
    restart: unless-stopped
    ports:
      - "5335:53/udp"
      - "5335:53/tcp"
    volumes:
      - /mnt/data/stack/unbound:/opt/unbound/etc/unbound

  # --- NAS ---
  samba:
    image: dperson/samba
    container_name: samba
    restart: unless-stopped
    environment: [USERID=1000, GROUPID=1000]
    ports: ["139:139", "445:445"]
    volumes:
      - /mnt/data/share:/share
    command: '-u "nasuser;%SMB_PASSWORD%" -s "Backup;/share;yes;no;no;nasuser"'
    # note: authenticated share, NOT guest-writable — see §6.2

  # --- Observability ---
  netdata:
    image: netdata/netdata
    container_name: netdata
    pid: host
    network_mode: host
    restart: unless-stopped
    cap_add: [SYS_PTRACE, SYS_ADMIN]
    security_opt: [apparmor:unconfined]
    volumes:
      - netdatalib:/var/lib/netdata
      - netdatacache:/var/cache/netdata
      - /etc/passwd:/host/etc/passwd:ro
      - /etc/group:/host/etc/group:ro
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro

volumes:
  netdatalib:
  netdatacache:
cd /mnt/data/stack && docker compose up -d
docker compose ps          # all four: running
ss -tulpn | grep -E ':53|:445|:19999'   # ports bound where expected

5. DNS Architecture in Depth: Filtering + Recursion

To achieve DNS sovereignty, we chain AdGuard Home (filtering, caching, per-client visibility) with Unbound (recursive resolution). Unbound queries the root nameservers and walks the delegation chain itself — . → .com → example.com — so no single upstream company (Google, Cloudflare) receives your complete browsing history.

5.1 Unbound Configuration Worth Understanding

# /mnt/data/stack/unbound/unbound.conf (key directives)
server:
  interface: 0.0.0.0
  port: 53
  do-ip6: no
  # privacy: send minimal qnames upstream (RFC 7816)
  qname-minimisation: yes
  # DNSSEC validation — forged answers are rejected, not served
  auto-trust-anchor-file: "/opt/unbound/etc/unbound/root.key"
  # cache tuning for a 16 GB Pi
  rrset-cache-size: 256m
  msg-cache-size: 128m
  cache-min-ttl: 300
  # serve stale answers while refreshing — smooths upstream blips
  serve-expired: yes
  prefetch: yes            # refresh popular records before expiry

5.2 Wiring the Chain

  1. Open AdGuard's web UI (http://192.168.1.5:3000) → Settings → DNS Settings.
  2. Set Upstream DNS servers to 127.0.0.1:5335 (the Unbound container).
  3. Enable DNSSEC display and select parallel-request mode.
  4. On the router: DHCP hands out 192.168.1.5 as primary DNS and a public resolver as secondary — the escape hatch for when you're maintaining the Pi.

5.3 Verify the Chain End to End

# Filtering works (known ad domain → blocked):
dig @192.168.1.5 doubleclick.net        # → 0.0.0.0

# Recursion works (Unbound answering directly):
dig @192.168.1.5 example.com +stats     # first query ~25-60 ms
dig @192.168.1.5 example.com +stats     # cached: ~0-1 ms

# DNSSEC validation works (deliberately-broken domain must FAIL):
dig @192.168.1.5 dnssec-failed.org      # → SERVFAIL  ✔ validation active

6. NAS Performance Tuning

6.1 Saturating the Gigabit Link

Default Samba configurations often deliver 60–80 MB/s on hardware capable of wire speed. These smb.conf directives enable async I/O and sendfile, reliably reaching ~115 MB/s sustained (the practical ceiling of 1 GbE):

socket options = TCP_NODELAY IPTOS_LOWDELAY SO_RCVBUF=65536 SO_SNDBUF=65536
read raw = yes
write raw = yes
min receivefile size = 16384
use sendfile = true
aio read size = 16384
aio write size = 16384

6.2 Security Note on Home Shares

Resist the guest-writable share. A NAS holding your backups is a ransomware target like any other: create a dedicated SMB user, require authentication, and export the share read-only to media players that only need to read. SMB signing stays on; SMB1 stays off (it is off by default — leave it that way).

7. Data Resilience: 3-2-1 with Restic

Adhering to the 3-2-1 rule — three copies, two media, one off-site — we use Restic for encrypted, deduplicated snapshots to both a local USB HDD and S3:

# One-time setup
restic -r /mnt/usb-backup/restic init
restic -r s3:s3.us-east-1.amazonaws.com/my-backup-bucket init

# /etc/cron.daily/homelab-backup
#!/usr/bin/env bash
set -euo pipefail
export RESTIC_PASSWORD_FILE=/mnt/data/stack/.restic-pass
DIRS="/mnt/data/share /mnt/data/stack"

restic -r /mnt/usb-backup/restic backup $DIRS --tag daily
restic -r s3:s3.us-east-1.amazonaws.com/my-backup-bucket backup $DIRS --tag daily

# Retention: 7 daily, 4 weekly, 6 monthly
restic -r /mnt/usb-backup/restic forget --keep-daily 7 --keep-weekly 4 \
       --keep-monthly 6 --prune

# Verify repository integrity (subset) — catches silent corruption early
restic -r /mnt/usb-backup/restic check --read-data-subset=5%

For the S3 tier, add a lifecycle rule transitioning objects to Glacier Deep Archive after 30 days — long-term retention at ~$1/TB/month. And note the last line: a backup system that is never checked is a hope, not a backup.

8. Hands-On Practicals: Benchmark It, Then Break It

8.1 Practical A — Benchmark the Stack (≈ 30 min)

# NVMe raw throughput (expect ~400-450 MB/s on PCIe 2.0 x1):
sudo hdparm -t /dev/nvme0n1

# Samba over the wire, from a client (expect ~110-117 MB/s):
dd if=/dev/zero of=/mnt/smb/testfile bs=1M count=2048 oflag=direct
dd if=/mnt/smb/testfile of=/dev/null bs=1M iflag=direct

# DNS latency distribution, 100 mixed queries:
for d in $(shuf -n 100 top-domains.txt); do
  dig @192.168.1.5 "$d" +stats | grep "Query time"
done | awk '{print $4}' | sort -n | awk '
  {a[NR]=$1} END {print "p50:", a[int(NR*.5)], "ms  p95:", a[int(NR*.95)], "ms"}'
# Typical: p50 ~1 ms (cache), p95 ~45 ms (cold recursive)

# Thermals under combined load (should hold < 70°C with active cooling):
watch -n 2 vcgencmd measure_temp

8.2 Practical B — DNS Failure Drill (≈ 15 min)

docker compose stop adguard
# From a client: browsing should CONTINUE (router's fallback resolver
# takes over within seconds). If the house lost internet, your router's
# secondary DNS is missing — fix that before anything else.
docker compose start adguard
dig @192.168.1.5 example.com   # chain restored  ✔

8.3 Practical C — Full Disaster Recovery Drill (≈ 45 min, do it BEFORE you need it)

  1. Pull the Pi's boot media entirely. Boot from the spare SD card image (§2's BOM line you almost skipped).
  2. Restore state: restic -r /mnt/usb-backup/restic restore latest --target /mnt/data
  3. Relaunch: cd /mnt/data/stack && docker compose up -d
  4. Re-run §5.3's verification. Time the whole drill — under 30 minutes is a pass. Write the actual steps you took into a RECOVERY.md beside the compose file; that document is now your most valuable artifact.

9. Operations Checklist

A homelab that provides DNS or storage becomes part of the household's critical path. Treat it like a small production system and document the recovery process before something breaks.

Recommended maintenance routine:
  • Export AdGuard and Unbound configuration after every meaningful change (and commit the compose directory to a private git repo).
  • Keep the spare boot device imaged and tested — §8.3 is only fast if this exists.
  • Monitor disk SMART status, temperature, memory pressure, and DNS query latency in Netdata; alert to your phone via its cloud relay or ntfy.
  • Test Restic restores monthly, not only backups.
  • Document router DHCP/DNS settings so the network can be restored quickly by someone who isn't you.
  • Patch deliberately: overlayroot-chrootapt upgrade → reboot, monthly, with the family forewarned.

10. FAQ

Should DNS and NAS run on the same Pi?
Fine for a lab — but configure the router's secondary DNS as a fallback so storage maintenance never takes down name resolution. A second DNS-only Pi is the single biggest reliability upgrade available.

Is NVMe worth it?
Decisively yes for NAS and container state: ~420 MB/s vs an SD card's ~40–90 MB/s, plus real endurance and SMART telemetry. SD is for experiments.

Why Unbound instead of just using Cloudflare upstream?
Recursion means no single company sees your complete query history, plus DNSSEC validation you control. Cost: tens of milliseconds on first lookup, then cache-speed.

How do I stop SD/boot corruption?
Read-only overlay root, high-write paths on NVMe, and a real power supply. After that, power cuts are a non-event.

What's the backup strategy?
3-2-1 via Restic: live NVMe, local USB HDD repo, S3/Deep Archive off-site — with a monthly restore drill, which is the part that makes it real.

Power cost?
4–6 W idle, ~12 W peak: roughly $7/year. The same services on a repurposed x86 tower run $150–400/year.

11. Conclusion

By leveraging the PCIe capabilities of the Raspberry Pi 5, consumer hardware becomes a genuinely capable network edge appliance: DNS you own end-to-end, wire-speed storage, observability that pages you before the family does, and — because we built the boring parts too — backups that restore and a recovery drill with a measured time. That last mile of operational discipline is what separates a homelab that teaches production engineering from a pile of containers that happens to work today.

Related Writings