featured image

Observability in a Sovereign Ecosystem: Deploying Uptime Kuma and Umami on TrueNAS SCALE for Zero-Trust Website Monitoring

This tutorial guides you through the deployment of an internal observability stack within a self-hosted personal cloud environment. Leveraging TrueNAS SCALE, ZFS, and a Tailscale mesh VPN, we will set up Uptime Kuma for robust service monitoring and Umami for privacy-respecting analytics.

Published

Tue Nov 11 2025

Technologies Used

Uptime Kuma Umami TrueNAS Scale Tailscale ZFS
Beginner 14 minutes

The irony of running a self-hosted personal cloud is that hiding your services from the internet also hides them from standard monitoring tools. External ping services like UptimeRobot can’t reach containers sitting behind a Tailscale mesh VPN. If Nextcloud crashes at 2 AM, you won’t know until you try to sync your files the next morning.

The fix is internal monitoring — tools that live inside the same network as your services and can reach them directly. This tutorial deploys two: Uptime Kuma for service health monitoring and Umami for privacy-respecting web analytics. Both get persistent ZFS storage so their data survives container restarts, and both run inside the Tailnet so you access their dashboards through the same zero-trust channel as everything else.

What You Need Before Starting

You should understand how to provision and permission ZFS datasets (specifically POSIX ACLs for container storage), basic Docker Compose syntax for volumes and environment variables, and how Tailscale’s internal IP addresses map to the node running your containers. This guide assumes TrueNAS SCALE (Dragonfish 24.04 or Electric Eel 24.10+) with a configured ZFS pool and Tailscale already running.

ZFS-Backed Persistence: The Non-Negotiable First Step

Running docker run -p 3001:3001 louislam/uptime-kuma:1 works, but a container restart wipes your monitoring history and alert configurations. Uptime Kuma stores everything — monitor configs, heartbeat history, alert rules — in a SQLite database inside the container. If that database isn’t on persistent storage, it’s gone on every restart.

Create the datasets first:

zfs create tank/apps/uptimekuma
zfs create tank/apps/umami
zfs create tank/apps/umami/db

chown -R 568:568 /mnt/tank/apps/uptimekuma
chown -R 568:568 /mnt/tank/apps/umami

The UID/GID 568 is TrueNAS SCALE’s default apps user. Setting ownership here prevents the container from starting and immediately failing with a permission error when it tries to write its database.

Deploying Uptime Kuma

services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    container_name: uptime-kuma
    restart: unless-stopped
    ports:
      - "3001:3001"
    volumes:
      - /mnt/tank/apps/uptimekuma:/app/data
    security_opt:
      - no-new-privileges:true

The ZFS dataset mount gives you the ARC benefit: Uptime Kuma commits to its SQLite database hundreds of times per hour (one commit per heartbeat check), and those small random writes would hammer a cheap SSD or SD card. ZFS coalesces them in RAM and flushes to disk in contiguous blocks, which dramatically reduces write amplification on the underlying storage.

Uptime Kuma is written in Node.js and uses a single-threaded async event loop. When it dispatches 20 HTTP requests to check your home lab’s services, it doesn’t block waiting for each response. It registers callbacks and moves on, which is why a nine-year-old CPU can monitor a dozen services while using under 50MB of RAM.

Deploying Umami with PostgreSQL

Umami requires a database backend. I’m using PostgreSQL rather than MySQL because the Alpine build is smaller and PostgreSQL’s write behavior works better with ZFS’s CoW model. The two containers communicate over an internal bridge network — PostgreSQL is completely isolated from the host.

services:
  umami-db:
    image: postgres:15-alpine
    container_name: umami_db
    restart: always
    environment:
      POSTGRES_DB: umami
      POSTGRES_USER: umami
      POSTGRES_PASSWORD: your_secure_password
    volumes:
      - /mnt/tank/apps/umami/db:/var/lib/postgresql/data
    networks:
      - umami-net

  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    container_name: umami
    restart: always
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://umami:your_secure_password@umami-db:5432/umami
      APP_SECRET: your_randomly_generated_secret
    depends_on:
      - umami-db
    networks:
      - umami-net

networks:
  umami-net:
    driver: bridge

Don’t hardcode the database password in production — use TrueNAS SCALE’s secret management in the UI, or at minimum use an environment file that isn’t committed to version control. Also: don’t rotate APP_SECRET after launch. Umami uses it to hash analytics data deterministically. Changing it breaks the hash chain and fragments your historical data.

Configuring Uptime Kuma’s Monitors

Because Uptime Kuma is running on the TrueNAS host, you reach your other containers using their local IPs or the TrueNAS host IP. To monitor Vaultwarden running on port 8080, add an HTTP(S) monitor pointing at http://192.168.1.100:8080.

One particularly useful configuration is “Upside Down Mode.” Instead of monitoring if an app is up, you monitor something that should be down and alert if it becomes accessible. For example: monitor your public IP’s SSH port (22). If port 22 ever becomes accessible from the public internet — which would be a serious security misconfiguration — Uptime Kuma triggers an outage alert. It’s a firewall breach detector disguised as a monitoring tool.

Alert Fatigue and the Retry Configuration

The default Uptime Kuma configuration alerts on the first failure. Don’t run it this way. If Nextcloud performs a background garbage collection and drops a single HTTP packet, you’ll get paged at 2 AM for a non-issue.

Set the monitor’s Retries to 3 and the Heartbeat Retry Interval to 20 seconds. A service has to fail three consecutive checks (one full minute) before Kuma sends a webhook to Discord or Slack. This eliminates false positives from transient network hiccups while still catching real outages quickly.

The Tailscale Split-Brain Problem

Here’s a scenario worth planning for: the TrueNAS server loses connection to the Tailscale control plane, but your ISP connection is still up. Your containers — Vaultwarden, Nextcloud — are healthy and running. But you can’t reach them from your laptop because Tailscale is down.

If Uptime Kuma’s alert webhooks route through Tailscale, they also fail during this event. You don’t get notified that Tailscale dropped.

The fix is simple: configure Uptime Kuma’s webhooks (Discord, Telegram, etc.) to route over the standard WAN connection rather than through Tailscale. Even if the Tailnet is down, TrueNAS can still reach the Discord API directly, and Kuma can send you a message: “Tailscale interface down on TrueNAS.” You’ll know within minutes rather than discovering it when you try to access your vault from the road.

We respect your privacy.

← View All Tutorials

Related Projects

    Ask me anything!