Skip to content

About

Ansible-deployed Raspberry Pi 5 home server — Home Assistant, MQTT/Zigbee, and a Prometheus + Grafana monitoring stack in Docker.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Raspberry Pi 5 Home Server

CI

Ansible-deployed home server for a Raspberry Pi 5. A single playbook run turns a clean Raspberry Pi OS Lite install into a smart-home host: it installs Docker, renders all configuration, and brings up a stack of services (automation, MQTT, monitoring) as Docker Compose containers.

The project is declarative and reproducible: image tags are pinned, secrets live in Ansible Vault, and role correctness is checked by linters and Molecule in CI.

Architecture

A control node (a laptop running Ansible) configures a single Raspberry Pi over SSH. The Pi runs one Docker Compose project under /opt/home-server; services talk to each other over the internal Compose bridge network, and only the required ports are published.

control node (Ansible)  ──ssh──▶  Raspberry Pi 5
                                     ├─ Docker Engine + Compose
                                     └─ /opt/home-server (docker-compose.yml + configs)
                                          ├─ Home Assistant ──┐
                                          ├─ Mosquitto (MQTT) ◀┘ (127.0.0.1:1883)
                                          ├─ Zigbee2MQTT ──▶ Mosquitto   (optional, needs dongle)
                                          ├─ Prometheus ──▶ scrape ──▶ Node Exporter / Blackbox
                                          │       ▲                     (Node Exporter also reads the
                                          │       │                      LibreSpeed textfile results)
                                          │       └── Grafana (provisioned datasource + dashboard)
                                          └─ librespeed.timer ──▶ librespeed-cli ──▶ textfile metrics

Monitoring flow: Node Exporter exposes host metrics and the Blackbox exporter continuously ICMP-probes public resolvers (latency + reachability). Internet bandwidth is measured by librespeed-cli (Ookla/speedtest.net is blocked on some ISPs), run by a systemd timer that writes results to Node Exporter's textfile collector. Prometheus scrapes everything and Grafana visualizes it via an auto-provisioned datasource and the "Home Server" dashboard. Bandwidth is sampled every librespeed_interval (default 1h; each test moves ~1 GB, so ≈24 GB/day); connection quality is sampled continuously (every blackbox_scrape_interval, default 20s).

Components

Service Image Port Purpose
Home Assistant ghcr.io/home-assistant/home-assistant:2026.6 8123 Smart-home automation hub. Runs in host network mode; reaches MQTT at 127.0.0.1:1883.
Mosquitto eclipse-mosquitto:2.0.22 1883 MQTT broker. Anonymous access on the LAN by default.
Zigbee2MQTT ghcr.io/koenkk/zigbee2mqtt:2.12.0 8080 Bridges a Zigbee coordinator to MQTT. Optional, needs a USB dongle; disabled by default.
LibreSpeed CLI github.com/librespeed/speedtest-cli v1.0.13 (host binary) — Internet bandwidth (download/upload). Run by librespeed.timer; writes to Node Exporter's textfile collector. Replaces Ookla, which is ISP-blocked here.
Blackbox exporter quay.io/prometheus/blackbox-exporter:v0.28.0 9115 Continuous ICMP probes (latency + reachability) to public resolvers. Needs NET_RAW.
Prometheus prom/prometheus:v3.12.0 9090 Metrics collection and storage (30-day retention).
Grafana grafana/grafana-oss:13.0.2 3000 Dashboards. Prometheus datasource and the "Home Server" dashboard are provisioned automatically.
cAdvisor ghcr.io/google/cadvisor:v0.53.0 8088 Per-container resource metrics. Optional, disabled by default (no per-container data on Pi cgroup v2 without a cmdline change + reboot).
Node Exporter quay.io/prometheus/node-exporter:v1.11.1 9100 Host metrics (CPU, RAM, disk, network).
Uptime Kuma louislam/uptime-kuma:1 3001 Uptime monitoring. Optional, disabled by default.
Tailscale tailscale/tailscale:stable — Mesh VPN for remote access. Optional, disabled by default.

Each service is toggled by a *_enabled flag in group_vars/all.yml; disabled services are excluded from the Compose file, the Prometheus config, and the firewall rules.

Repository layout

.
├── ansible.cfg                  # Ansible settings (inventory, vault password file)
├── site.yml                     # playbook: base → docker → rescue → home_server roles
├── upgrade.yml                  # playbook: OS upgrade + verified reboot
├── requirements.yml             # Galaxy collections (community.docker/general, ansible.posix)
├── inventory/
│   └── hosts.yml.example        # inventory template (real hosts.yml is gitignored)
├── group_vars/
│   └── all.yml                  # all configuration: services, ports, images, tuning
├── vault.yml                    # secrets (Ansible Vault, encrypted)
├── vault.example.yml            # secrets template
├── roles/
│   ├── base/                    # OS: packages, hostname, timezone, avahi, UFW
│   ├── docker/                  # Docker Engine, Compose plugin, daemon tuning
│   ├── rescue/                  # dropbear, watchdog, self-check, journal, backup, serial console
│   └── home_server/             # directories, config rendering, stack deployment
├── molecule/default/            # test scenario (converge/idempotence/verify)
└── .github/workflows/ci.yml     # CI: lint + molecule

Roles

base — brings the OS to a working state: asserts a Debian-family host, updates the system, installs base packages, sets hostname and timezone, enables avahi-daemon, and optionally configures UFW (the allowed-port list is derived from the enabled services).

docker — installs Docker Engine and the Compose plugin from Docker's official repository, enables the service, adds the user to the docker group, and — if docker_daemon_options is set — writes /etc/docker/daemon.json (e.g. MTU) and restarts the daemon.

rescue — makes sure you never lose access to the headless host again (see Resilience): a second SSH server (dropbear) on another port, the hardware watchdog, a self-check timer with a dead-man's-switch ping, a persistent journal, a daily backup of the non-reproducible state, and the serial console on the Pi 5 debug UART.

home_server — resolves the Zigbee dongle path (when Zigbee2MQTT is enabled), creates the directory tree under /opt/home-server, renders every config (Mosquitto, Zigbee2MQTT, Prometheus, Grafana provisioning, docker-compose.yml), deploys the stack via community.docker.docker_compose_v2, and waits for services to become ready on their ports (Home Assistant additionally gets an HTTP readiness check).

Quickstart

Prerequisites

  • A Raspberry Pi running Raspberry Pi OS Lite 64-bit, with SSH key access and passwordless or -K-prompted sudo.
  • Ansible on the control machine (pip install ansible-core or brew install ansible).
  • For Zigbee2MQTT: a USB Zigbee coordinator plugged into the Pi.

1. Install collections

ansible-galaxy collection install -r requirements.yml

2. Point Ansible at your Pi

cp inventory/hosts.yml.example inventory/hosts.yml
# edit inventory/hosts.yml — set ansible_host and ansible_user
# inventory/hosts.yml
all:
  hosts:
    pi:
      ansible_host: 192.168.1.10
      ansible_user: youruser
      ansible_python_interpreter: /usr/bin/python3

3. Create the vault with your secrets

cp vault.example.yml vault.yml
# fill in real values, e.g.:
#   vault_grafana_admin_password: "$(openssl rand -base64 24)"
echo 'your-vault-password' > .vault_pass && chmod 600 .vault_pass
ansible-vault encrypt vault.yml

4. Review configuration

Open group_vars/all.yml and check at least pi_hostname, server_timezone, the *_enabled flags, and zigbee_adapter if you use Zigbee.

5. Deploy

ansible all -m ping                 # connectivity check
ansible-playbook site.yml -K        # -K prompts for the sudo password

The first run pulls multi-gigabyte images and can take a while. Re-running is idempotent.

6. Access the services (replace <pi> with your Pi's address)

Home Assistant     http://<pi>:8123
Grafana            http://<pi>:3000      (admin + vault_grafana_admin_password)
Prometheus         http://<pi>:9090/targets
Blackbox exporter  http://<pi>:9115

The bandwidth test runs on a timer (librespeed.timer); to run it once immediately: systemctl start librespeed.service. Check the timer with systemctl list-timers librespeed.timer and results in /opt/home-server/textfile/librespeed.prom.

Internet speed lives on the Grafana "Pi Home Server Overview" dashboard: the "Download/Upload (latest)" stats and the "Speedtest bandwidth" graph for throughput (from LibreSpeed), and "Internet latency" / "Internet reachability" for connection quality.

Day-2 operations (on the Pi, in /opt/home-server)

docker compose ps           # status
docker compose logs -f      # logs
docker compose up -d        # apply changes / restart
docker compose down         # stop the stack

OS upgrades — never a side effect of site.yml (base_do_full_upgrade: false). Run the dedicated playbook, which upgrades, reboots and then proves the host came back (OpenSSH and dropbear banners, dpkg --audit, Docker, every compose service running, the rescue self-check):

ansible-playbook upgrade.yml -K

An upgrade that only breaks on the next boot is caught here, while you are at the keyboard — not months later at the first power cycle.

Resilience

Why: in October 2026 the Pi came back from a summer of being unplugged with OpenSSH accepting connections and closing them before the banner, four of six containers down, and no way in — one SSH implementation, no console, and the last apt full-upgrade had never been followed by a reboot. The rescue role adds independent layers so that one failure never means "pull the SSD":

Layer What it does Knob
Dropbear rescue SSH Second SSH server, different implementation, own host key, port 2222. A broken OpenSSH (interrupted upgrade, bad config, lost keys) does not touch it: ssh -p 2222 user@pi-home.local. rescue_dropbear_*
Hardware watchdog systemd pets /dev/watchdog; a hung kernel/systemd reboots itself. rescue_watchdog_*
Self-check timer Every 5 min: sshd banner on 127.0.0.1:22, Docker daemon, root fs writable and below 90 %. Consecutive sshd failures → systemctl restart ssh after 3, reboot after 6 (at most once a day). Logs to the journal as rescue-check. rescue_check_*
Dead man's switch The self-check pings a healthchecks.io URL on success and …/fail with details on failure. Silence = the host is sick or dead = Telegram/e-mail alert. Grafana on the Pi cannot report that the Pi itself is down; this can. vault_healthcheck_url
Persistent journal Storage=persistent, 200 MB cap, so a broken boot can be read afterwards (journalctl -b -1). rescue_journal_*
Backup Daily tarball of homeassistant/ (incl. .storage, the entity registry) and zigbee2mqtt/data/ to /var/backups/home-server, newest 7 kept, optional rsync to another host. Everything else is reproduced by site.yml. rescue_backup_*
Serial console Keeps console=serial0,115200 in cmdline.txt. Plug a Raspberry Pi Debug Probe into the 3-pin UART between the micro-HDMI ports, run screen /dev/tty.usbmodem* 115200 on the laptop, and you have a login prompt plus the full boot log whatever sshd or the network are doing. rescue_serial_console
Wired only Optional dtoverlay=disable-wifi: one interface, one IP, one router segment. rescue_disable_wifi

Operational rules that no role can enforce: shut down with the power button or sudo poweroff, never by pulling the plug; keep a DHCP reservation for the Pi on the router; run upgrade.yml for upgrades.

Restore from a backup after a rebuild:

ansible-playbook site.yml -K                                   # fresh host, empty stack
docker compose -f /opt/home-server/docker-compose.yml stop homeassistant zigbee2mqtt
tar -C /opt/home-server -xzf /var/backups/home-server/home-server-<stamp>.tar.gz
docker compose -f /opt/home-server/docker-compose.yml up -d

Configuration

All user-facing configuration lives in group_vars/all.yml:

  • Host: pi_hostname, server_timezone, server_user, base_dir.
  • Services: *_enabled flags and *_port ports.
  • Images: tags are pinned (no :latest) for reproducibility.
  • Docker: docker_daemon_options — contents of daemon.json (defaults to mtu: 1280 and max-concurrent-downloads: 1, see Networking).
  • Service settings: Prometheus retention, LibreSpeed interval, Zigbee adapter type, etc.

The inventory (inventory/hosts.yml) holds the Pi's real address and user and is intentionally not committed — only hosts.yml.example is in the repo.

Testing & CI

  • yamllint and ansible-lint (basic profile) — static analysis.
  • Molecule (molecule/default) — converge → idempotence → verify of the base, docker and rescue roles in a systemd-enabled Debian 12 container. The full service stack is not tested in CI (it needs a Zigbee dongle and multi-GB images).
  • GitHub Actions (.github/workflows/ci.yml) — lint and molecule jobs on push to main and on PRs. A repository secret ANSIBLE_VAULT_PASSWORD is required so CI can decrypt the vault.

Tasks that can't run inside a container (editing /etc/hostname, /etc/hosts) are tagged molecule-notest; they are skipped only under Molecule and run normally on a real host.

Security model

The defaults assume a trusted LAN:

  • Mosquitto allows anonymous connections (mosquitto_allow_anonymous: true).
  • UFW is disabled; when enabled, the open TCP port list is built from the active services (plus the dropbear rescue port).
  • External access (Tailscale) is disabled.

To harden: enable UFW, configure Mosquitto authentication/ACLs, and use Tailscale instead of exposing ports. The Blackbox exporter port (9115) only needs to be reachable by Prometheus on the Docker network — you can drop it from the published ports if you don't need to curl it directly.

Networking

Image pulls from some CDNs can stall on double-NAT/PPPoE links because of a Path-MTU-Discovery black hole: the TLS handshake succeeds but large layers hang. The fix is to lower Docker's MTU (docker_daemon_options.mtu, default 1280) — the docker role writes this to daemon.json before deploying. A network-wide fix is MSS clamping on the router's WAN interface. If the IPv6 route to a registry is broken, restrict Docker to IPv4.

About

Ansible-deployed Raspberry Pi 5 home server — Home Assistant, MQTT/Zigbee, and a Prometheus + Grafana monitoring stack in Docker.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages