Container-Based Multi-Function Home Server
Personal Project
Overview
Built a 10-container home server as Infrastructure-as-Code on a Raspberry Pi 5 (8GB RAM) with a 2TB NVMe SSD running Ubuntu Server 24.04 LTS arm64, using Docker Compose and shell scripts. Consolidates smart-home hub, Time Machine backups, web change detection, uptime monitoring, and an off-site VPN into a single box, with all configuration Git-managed so the environment can be rebuilt from scratch cheaply.
Architecture
Uses Docker Compose profiles (`phase1`–`phase4` and `full`) to bring the stack up in dependency order. During validation only the relevant phase is started; in production `full` starts everything at once. Each phase has a matching idempotent setup script, so the state can always be reproduced from the repo.
Bridge and Host networks are used deliberately for different roles. Containers with layer-2 requirements (Samba, Homebridge, Home Assistant — SMB, mDNS, Bluetooth) run on the host network; plain HTTP services are isolated on a bridge. The Homepage dashboard's `siteMonitor` resolves bridge services by container name and host services by the Docker bridge gateway IP, giving a single monitoring surface across both worlds.
Proxy hosts on Nginx Proxy Manager are configured declaratively by hitting its REST API with a Bearer token; state touched through the web UI is read back and written into `configs/`, so ad-hoc changes still land in IaC. Avahi publishes CNAMEs (`/etc/avahi/cname.d/services.conf`) that map `<service>.local -> ubuntu-pi.local` on the fly, giving every service a `.local` hostname without polluting the household DNS.
- phase1 platform: Nginx Proxy Manager / Docker Socket Proxy (read-only socket exposure for Homepage) / Homepage / MkDocs Material
- phase2 files: Samba (Time Machine with full `vfs_fruit` support)
- phase3 smart home: Homebridge (HomeKit + Tesla integration) / Home Assistant (`privileged` + host network + D-Bus)
- phase4 monitoring: Playwright Chrome (JS rendering) / Changedetection.io / Uptime Kuma
- Installed directly on the host: Tailscale (Exit Node) / AdGuard Home (coexists with systemd-resolved by disabling the stub listener) / Rclone (daily cron backup)
Operations & Observability
All shell scripts from `phase0-prereq.sh` through `phase4-utility.sh` are idempotent. State checks are inserted before `systemctl enable --now` and `docker compose up -d`, so re-runs only apply deltas and recovery from misconfiguration is fast.
`verify-all.sh` runs 13 health checks in one shot — port reachability, container status, mDNS resolution, SMB connectivity, Home Assistant API — making it trivial to pinpoint which layer is failing right after a deploy.
Rclone syncs `homeassistant` and `timemachine` to Google Drive at 03:00 daily, throttled to 5 MB/s to avoid saturating upstream bandwidth, giving on-box + cloud redundancy. Persistent data lives under `~/data/` (`.gitignored`), physically separated from config under `configs/` so backups never pull in source artifacts.
Hardware Tuning
Enabled PCIe Gen3 by appending `dtparam=pcie1_gen=3` to `/boot/firmware/config.txt`, roughly doubling NVMe SSD throughput (~450 MB/s -> ~900 MB/s) and effectively halving Time Machine backup times. The EEPROM was also flashed to the latest release to keep NVMe boot stable.
Because AdGuard Home needs UDP/53, the `systemd-resolved` stub listener is disabled via `/etc/systemd/resolved.conf` and `/etc/resolv.conf` is pointed at AdGuard, eliminating the port conflict between the OS default resolver and the household DNS filter.