Skip to content
Infrastructure

Consolidating homelab alerting with Apprise

By Victor Da Luz
apprise alerting homelab prometheus alertmanager discord proxmox routeros uptime-kuma

As the homelab grew, alerts stopped being a single story. Prometheus and Alertmanager handled metrics. RouterOS ran its own scripts. Proxmox could notify on its own. The QNAP only spoke SMTP. Each path had different formats, different destinations, and different failure modes. I wanted one place to receive everything, route by severity where it mattered, and keep email-only gear on the bus without running five parallel notification systems.

This is how I landed on Apprise, what sits around it, and the integration bugs that ate an afternoon or two.

The problem

Fragmented alerting is hard to reason about. You cannot tell whether you are noisy or blind if every stack ships notifications differently. Some services want webhooks. Some only want email. Some emit JSON nobody else understands.

I needed a single notification hub that could talk to Discord (my primary destination), accept Alertmanager’s native payload, accept RouterOS and Proxmox webhooks, and turn QNAP email into the same pipeline. Bonus points if the API stayed dead simple for future services.

Choosing Apprise

I looked at the usual self-hosted options: Apprise, Gotify, ntfy, and a few others. Apprise won because it already maps to a huge list of backends (Discord and email included), exposes a straightforward REST API, integrates cleanly with Alertmanager when you give it the shape it expects, and I had already run it successfully in an earlier homelab iteration.

I deploy Apprise in its own privileged LXC with Docker-in-Docker, separate from the Prometheus/Grafana stack. That isolation keeps a monitoring outage from taking down notifications, and vice versa. The container is sized modestly: one vCPU, half a gig of RAM, small disk—enough for a webhook relay.

Discord is split into two webhooks: one for alerts I care about when something is wrong or worth acting on, and one for lower-priority noise. Apprise routes into those based on how I configure each persistent store key.

Email-only gear: SMTP relay in front of Apprise

QNAP will not send a webhook. It will send mail. Rather than run a full mailbox stack, I put a small Postfix relay on a separate LXC. QNAP sends to a relay host on the LAN. Postfix aliases a virtual address to a pipe transport that runs a short Python script (qnap-transformer.py). The script reads the message from stdin, cleans up QNAP’s usual cruft (extra Message: lines, headers and footers you do not want in Discord), pulls subject and body, and POSTs to Apprise’s QNAP endpoint.

No mailbox, no IMAP, no polling. Mail arrives, gets transformed, and lands in Discord with everything else. That is the whole idea.

Update: I later replaced that Postfix relay with Mailrise for the same QNAP → Apprise hop; Migrating my SMTP relay from Postfix to Mailrise has the story and why I stopped fighting the container.

Integrations that mattered

Alertmanager does not speak Apprise’s JSON. I run a small transformer service in the Prometheus stack that accepts Alertmanager’s webhook, formats a human-readable title and body (including grouped alerts), and forwards to Apprise. Alertmanager → transformer → Apprise → Discord. That keeps Alertmanager configuration standard.

RouterOS scripts post to a dedicated Apprise URL. I deploy those scripts with Ansible so they stay versioned like the rest of the network.

Proxmox uses Datacenter notifications pointed at an Apprise webhook. Same pattern: webhook target, persistent store key, Discord behind it.

Uptime Kuma needed a trick. Apprise wants a body field; Kuma’s default JSON uses different field names. Apprise can remap fields via query parameters on the webhook URL (:msg=body and :name=title style). That let me stick with Kuma’s default payload instead of fighting its custom body editor.

Traefik exposes the Apprise UI on a public hostname with a wildcard cert. Apprise serves static assets through nginx on one port and the API through gunicorn on another; the reverse proxy has to hit the nginx front so the web UI loads CSS and JS reliably. Routing straight to the application port alone broke the UI in practice.

Internal DNS keeps human-friendly names (apprise.lan, smtp-relay.lan) so services do not depend on brittle IP addresses in every config.

What broke (and how I fixed it)

Alertmanager payload shape was the big design issue. The transformer exists because asking Alertmanager to emit Apprise’s format directly would be brittle. A small intermediate service is easier to test and change than forked Alertmanager templates.

Uptime Kuma field names were the second lesson. The URL remapping feature sounds obscure until you need it; then it saves you from unmaintainable JSON templates in Kuma.

RouterOS scripts originally included a tag field. Apprise’s persistent store configuration did not match those tags, so notifications silently disappeared. Removing the tag from the RouterOS scripts was simpler than over-configuring tags on the Apprise side.

QNAP through Postfix was a plumbing exercise: correct virtual aliases, transport maps, making sure the Python environment had requests on the relay host, and verifying qnap-transformer.py stripped the boilerplate so Discord saw the alert, not the email template. Once the pipe path was consistent, end-to-end tests hit Discord like any other source.

Traefik to Apprise needed the nginx hop for static assets. That is a subtle one—if the UI looks broken, check which upstream port you are actually proxying.

Hardening after the first pass

Once the stack was up, I tightened a few things during a code review: pinned the Docker image (caronc/apprise:v1.7.0 instead of floating latest), locked down apprise.yml permissions to 0600, removed deprecated shell templating in favor of Ansible for config generation, and added a post-deploy health check so a bad deploy fails fast instead of failing silently.

Verification

I verified each path independently: trigger a test alert from Alertmanager, fire a RouterOS script, send a mail through the relay, and flip a Uptime Kuma check. When every path shows up in the right Discord channel with the expected formatting, the consolidation is real.

Lessons learned

Separate the notifier from the monitoring stack. Coupling them saves a few milliseconds of network path and costs a lot of coupling when things go wrong.

Email-to-webhook is fine if you control the relay and keep the script boring.

Payload transformation is normal. Not every upstream will speak your JSON; a tiny adapter beats hacking every sender.

When in doubt, simplify. Dropping RouterOS tag fields beat debugging Apprise tag filters for an afternoon.

Reverse proxy details matter for apps that split static assets and API across processes.

Apprise is not magic—it is a solid router. The work is in the edges: transformers, SMTP pipes, field remapping, and making sure your proxy points at the right port. After that, one inbox is enough.

Related reading

Ready to Transform Your Career?

Let's work together to unlock your potential and achieve your professional goals.