Skip to content

Notify Stack: DIUN + ntfy

The Notify Stack centralises two notification building blocks of the infrastructure:

ComponentRoleImage
DIUNDocker image update detectorcrazymax/diun
ntfySelf-hosted push notification serverbinwiederhier/ntfy

Notification channels

N8N · Docker DIUN workflow

notify-stack

webhook Bearer

webhook

/var/run/docker.sock

base-images.yml · file provider

DIUN · scan every 6 h

ntfy · auth deny-all

/webhook/notify/image-update

Router · critical / base / app

DIUN Update Executor

Telegram

Discord

Mobile · ntfy app


Why DIUN over Watchtower or the Docker Hub UI?

Section titled “Why DIUN over Watchtower or the Docker Hub UI?”
CriterionDIUNWatchtowerDocker Hub UI
Detection without pullYes (compares digests)No (pulls to verify)Manual
Auto-updateNo (notification only)Yes (but blind)No
File providerYes (custom base images)NoNo
Customisable webhookYes (Bearer auth)NoNo
Label-based exclusionYesYesN/A

Decision criteria:

  1. Decouple detection from update — Watchtower applies blindly, DIUN notifies and delegates the decision to an N8N workflow that can classify (critical / base / app) and request Telegram approval.
  2. Watch base images of custom Dockerfiles — The file provider lets you detect that a new version of caddy:builder has shipped even though no container runs that image directly.
  3. No useless pull — DIUN compares digests via the registry API, without downloading the entire image.
Criterionntfy self-hostedPushoverSlack
CostFree (VPS)Per-app feeFree / paid
SovereigntyData on my own VPSSaaSSaaS
Simple APIcurl HTTPProprietary APIProprietary API
AuthToken + user/passTokenOAuth + tokens
Multi-channelTopicsDevicesChannels

The goal is to have a notification channel separate from Telegram for monitoring alerts (Alertmanager, Grafana). Telegram is reserved for interactive flows (N8N workflows, approvals); ntfy is for silent mobile push.


The service runs without a UI, configured entirely through environment variables.

# notify-stack/docker-compose.yaml (DIUN excerpt)
diun:
image: crazymax/diun:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- notify-diun-data:/data
- ./diun/base-images.yml:/config/base-images.yml:ro
environment:
- DIUN_WATCH_WORKERS=10
- DIUN_WATCH_SCHEDULE=0 */6 * * *
- DIUN_WATCH_FIRSTCHECKNOTIF=false
- DIUN_PROVIDERS_DOCKER=true
- DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=true
- DIUN_PROVIDERS_DOCKER_WATCHSTOPPED=false
- DIUN_PROVIDERS_FILE_FILENAME=/config/base-images.yml
- DIUN_NOTIF_DISCORD_WEBHOOKURL=${DISCORD_WEBHOOK_URL}
- DIUN_NOTIF_WEBHOOK_ENDPOINT=${N8N_WEBHOOK_URL}
- DIUN_NOTIF_WEBHOOK_HEADERS_AUTHORIZATION=Bearer ${DIUN_WEBHOOK_TOKEN}
ProviderSourcePost-detection behaviour
dockerRunning containersAuto-update if category critical / base / Telegram approval if app
filebase-images.yml (FROM images of custom Dockerfiles)Notification + manual rebuild or approval

The base-images.yml file lists watched images that are not used directly by any container:

- name: docker.n8n.io/n8nio/n8n:latest # base of n8n-custom
- name: caddy:builder # base of caddy-crowdsec
- name: caddy:latest
- name: node:20-slim # base of cli-ollama
- name: ghcr.io/astral-sh/uv:latest
ntfy:
image: binwiederhier/ntfy:latest
command: serve
volumes:
- ntfy-cache:/var/cache/ntfy
- ntfy-data:/var/lib/ntfy
- ./ntfy/server.yml:/etc/ntfy/server.yml:ro

The server.yml enforces auth-default-access: deny-all: every topic must be published by an authenticated user. Two modes coexist:

ModeUse case
Bearer tokenAPI calls from Alertmanager, N8N, scripts
User / passwordMobile app authentication
Fenêtre de terminal
# User management (from the VPS)
docker exec ntfy ntfy user list
docker exec -e NTFY_PASSWORD="..." ntfy ntfy user add --role=admin guillaume
# API tokens
docker exec ntfy ntfy token add --label="alertmanager" alertmanager-bot
docker exec ntfy ntfy token list
TopicProducersConsumers
alertsAlertmanager, Notification Hub (fallback)Mobile, Telegram
updatesDocker DIUN workflowMobile
monitoringGrafanaMobile

Publishing a message:

Fenêtre de terminal
# From the outside (HTTPS via Caddy)
curl -H "Authorization: Bearer tk_xxx" \
-H "Title: Disk Alert" \
-H "Priority: high" \
-H "Tags: warning" \
-d "Disk usage above 80%" \
https://ntfy.guigpap.com/alerts
# From the inside (Docker network, from N8N)
curl -H "Authorization: Bearer tk_xxx" \
-d "Message" \
http://ntfy:80/alerts

DIUN does not apply any update: it posts an authenticated webhook to N8N, which makes every decision.

docker

file

critical

base

app

DIUN scan · 0 */6 * * *

/webhook/notify/image-update · Bearer auth

Switch Provider · docker / file

Docker Inspect

Lookup Policy · Data Table image_policies

Category?

Critical · immediate update

Base · queue 03h-05h

App · Telegram approval

DIUN Update Executor · SW-1

Notification Hub

The image_policies table (managed in N8N Data Tables) defines the per-image behaviour. See the Docker Updates workflow article for the details of the sub-workflows and the auto-rebuild logic.

Fenêtre de terminal
# Stack management
docker compose -f notify-stack/docker-compose.yaml up -d
docker compose -f notify-stack/docker-compose.yaml logs -f
# DIUN — force an immediate scan
docker exec diun diun --run
# DIUN — list watched images
docker exec diun diun image list
# ntfy — publish from inside the container (test)
docker exec ntfy ntfy publish mytopic "Test message"

LimitImpactMitigation
DIUN does not push to ntfy directlyEvery mobile notification flows through N8NAdd DIUN’s native ntfy notifier later
No DIUN UIVisibility only via logsThe Docker DIUN workflow acts as the dashboard on the Telegram side
ntfy single-nodeNo HAAuto-restart + container monitoring
Discord webhook quotaSpam possible if many images moveDIUN groups notifications per scan

If notification volume grows:

  • Configure DIUN’s native ntfy notifier (direct output, without going through N8N for informational notifications).
  • Enable DIUN bundling (group several detections into a single notification per scan).

If I want a fallback in case N8N is down:

  • DIUN can write in parallel to Discord (already done) and ntfy (to add), guaranteeing the trail even if the N8N workflow crashes.

If I expose ntfy to other users:

  • Create per-topic ACLs (separate read/write).
  • Enable global rate limiting in server.yml.
MetricSourceAttention threshold
DIUN detections per scandocker logs diunUnusual spike = mass tag rotation by an upstream maintainer
DIUN webhook → N8N failuresDIUN logs + N8N DLQ> 1 per scan = check auth or N8N availability
Excluded containersdocker exec diun diun image listVerify regularly that nothing important is diun.enable=false by mistake