--- title: 'Notify Stack: DIUN + ntfy' url: https://blog.guigpap.com/en/infrastructure/notify-stack/ url_md: https://blog.guigpap.com/en/infrastructure/notify-stack.md category: infrastructure date: '2026-05-04' maturite: production techno: - docker - n8n application: - infrastructure - operations --- # Notify Stack: DIUN + ntfy > Docker image update monitoring and self-hosted push notifications ## 1. What? — Definition and context The **Notify Stack** centralises two notification building blocks of the infrastructure: | Component | Role | Image | |-----------|------|-------| | **DIUN** | Docker image update detector | `crazymax/diun` | | **ntfy** | Self-hosted push notification server | `binwiederhier/ntfy` | > **Note - Why both together** > > DIUN does not push directly to mobile: it triggers a webhook consumed by an N8N workflow. ntfy provides the mobile channel (and the monitoring fallback channel) at the end of the chain. Both services share the same "notification" theme, but they don't talk to each other directly today. ### Architecture diagram ```mermaid flowchart TD Socket["/var/run/docker.sock"] BaseImages["base-images.yml · file provider"] subgraph Notify["notify-stack"] direction TB DIUN["DIUN · scan every 6 h"] NTFY["ntfy · auth deny-all"] end subgraph N8N["N8N · Docker DIUN workflow"] direction TB HubWebhook["/webhook/notify/image-update"] Router["Router · critical / base / app"] Updater["DIUN Update Executor"] end subgraph Outputs["Notification channels"] direction TB Telegram["Telegram"] Discord["Discord"] Mobile["Mobile · ntfy app"] end Socket --> DIUN BaseImages --> DIUN DIUN -->|webhook Bearer| HubWebhook DIUN -->|webhook| Discord HubWebhook --> Router --> Updater Updater --> Telegram Updater --> NTFY NTFY --> Mobile ``` --- ## 2. Why? — Stakes and motivations ### Why DIUN over Watchtower or the Docker Hub UI? | Criterion | DIUN | Watchtower | Docker Hub UI | |-----------|------|------------|---------------| | **Detection without pull** | Yes (compares digests) | No (pulls to verify) | Manual | | **Auto-update** | No (notification only) | Yes (but blind) | No | | **File provider** | Yes (custom base images) | No | No | | **Customisable webhook** | Yes (Bearer auth) | No | No | | **Label-based exclusion** | Yes | Yes | N/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. ### Why ntfy over Pushover or Slack? > **Tip - ntfy in a nutshell** > > **ntfy** is a minimalist HTTP Pub/Sub server: you POST a message to `https://ntfy.example.com/`, and any client subscribed to that topic receives it. The official mobile app (Android/iOS) listens in the background and surfaces native notifications. | Criterion | ntfy self-hosted | Pushover | Slack | |-----------|------------------|----------|-------| | **Cost** | Free (VPS) | Per-app fee | Free / paid | | **Sovereignty** | Data on my own VPS | SaaS | SaaS | | **Simple API** | curl HTTP | Proprietary API | Proprietary API | | **Auth** | Token + user/pass | Token | OAuth + tokens | | **Multi-channel** | Topics | Devices | Channels | 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. --- ## 3. How? — Technical implementation ### DIUN configuration The service runs without a UI, configured entirely through environment variables. ```yaml # 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} ``` ### Two providers, two logics | Provider | Source | Post-detection behaviour | |----------|--------|--------------------------| | **docker** | Running containers | Auto-update if category `critical` / `base` / Telegram approval if `app` | | **file** | `base-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: ```yaml - 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 configuration ```yaml 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: | Mode | Use case | |------|----------| | **Bearer token** | API calls from Alertmanager, N8N, scripts | | **User / password** | Mobile app authentication | ```bash # 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 ``` ### Topics in use | Topic | Producers | Consumers | |-------|-----------|-----------| | `alerts` | Alertmanager, Notification Hub (fallback) | Mobile, Telegram | | `updates` | Docker DIUN workflow | Mobile | | `monitoring` | Grafana | Mobile | Publishing a message: ```bash # 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 ``` ### Integration with the Docker DIUN workflow DIUN does not apply any update: it posts an authenticated webhook to N8N, which makes every decision. ```mermaid flowchart TD D["DIUN scan · 0 */6 * * *"] Webhook["/webhook/notify/image-update · Bearer auth"] Switch["Switch Provider · docker / file"] Inspect["Docker Inspect"] Lookup["Lookup Policy · Data Table image_policies"] Cat{"Category?"} Critical["Critical · immediate update"] Base["Base · queue 03h-05h"] Approval["App · Telegram approval"] Exec["DIUN Update Executor · SW-1"] Hub["Notification Hub"] D --> Webhook --> Switch Switch -->|docker| Inspect --> Lookup --> Cat Switch -->|file| Hub Cat -->|critical| Critical --> Exec Cat -->|base| Base --> Exec Cat -->|app| Approval --> Exec Exec --> Hub ``` The `image_policies` table (managed in N8N Data Tables) defines the per-image behaviour. See the [Docker Updates](/en/workflows/docker-updates/) workflow article for the details of the sub-workflows and the auto-rebuild logic. ### Operational commands ```bash # 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" ``` --- ## 4. What if? — Outlook and limits ### Current limits | Limit | Impact | Mitigation | |-------|--------|------------| | **DIUN does not push to ntfy directly** | Every mobile notification flows through N8N | Add DIUN's native ntfy notifier later | | **No DIUN UI** | Visibility only via logs | The Docker DIUN workflow acts as the dashboard on the Telegram side | | **ntfy single-node** | No HA | Auto-restart + container monitoring | | **Discord webhook quota** | Spam possible if many images move | DIUN groups notifications per scan | ### Evolution scenarios **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`. ### Metrics to watch | Metric | Source | Attention threshold | |--------|--------|---------------------| | DIUN detections per scan | `docker logs diun` | Unusual spike = mass tag rotation by an upstream maintainer | | DIUN webhook → N8N failures | DIUN logs + N8N DLQ | > 1 per scan = check auth or N8N availability | | Excluded containers | `docker exec diun diun image list` | Verify regularly that nothing important is `diun.enable=false` by mistake | --- ## Related pages ### Infrastructure - [VPS Architecture](/en/infrastructure/architecture-vps/) — Big picture - [Security Stack](/en/infrastructure/security-stack/) — Caddy routes `ntfy.guigpap.com` - [Monitoring Stack](/en/infrastructure/monitoring-stack/) — Alertmanager → ntfy ### Workflows - [Docker Updates](/en/workflows/docker-updates/) — Auto-update and approval logic - [Notification Hub](/en/workflows/notification-hub/) — Centralised notification routing ### Reference - [Glossary](/en/reference/glossary/) — Webhook, Bearer token ## Metadonnees agent - Cet article est issu du blog GuiGPaP Lab. - Contexte global du blog: https://blog.guigpap.com/llms.txt - Contact auteur: https://odoo.guigpap.com/mon-cv - Licence: CC-BY-SA 4.0