# vds-ntfy-push ## Goal Self-host [ntfy.sh](https://ntfy.sh) на VDS для push-нотификаций на телефон. Docker-image `binwiederhier/ntfy`. Использовать для: - [[vds-backup-rsync-kreknin]] backup status (вместо/параллельно email на vitya.kuznetsov@gmail.com) - Monitoring alerts (CMS down, traefik certs истекают, disk fill, registry GC fail, etc.) - Любые ad-hoc уведомления от ops-скриптов Domain: `ntfy.vds.kzntsv.site` (под уже существующим wildcard `*.vds.kzntsv.site`). ## Open questions - [x] ~~**Auth model:**~~ per-topic ACL + admin user `vitya` (resolved 2026-05-20). Реализация: `NTFY_AUTH_DEFAULT_ACCESS=deny-all` + `ntfy user add --role=admin vitya`. Admin role → rw to all topics, per-topic ACL не нужен. Anon (no creds) → 403 на publish и subscribe ✅. - [x] ~~**Топики:**~~ начали с двух: `vds-backup` (для vds-backup-rsync-kreknin) + `vds-ops` (general alerts: cert expiry, disk fill, container crashes). Можно добавлять без миграции — admin role даёт доступ ко всему автоматом. - [x] ~~**Phone app:**~~ Android (resolved 2026-05-20) → ntfy Android app, pure self-host без APNs middleware. - [x] ~~**Persistence:**~~ sqlite в `/opt/stacks/ntfy/data/` (cache.db + auth.db). Volume mount `./data:/var/cache/ntfy`. - [x] ~~**Phone-side smoke:**~~ confirmed 2026-05-20 22:54 MSK — оба push'а («Topic renamed», «Password updated») пришли в Android ntfy app, screenshot в conversation. ## Implementation sketch `/opt/stacks/ntfy/docker-compose.yml`: ```yaml services: ntfy: image: binwiederhier/ntfy container_name: ntfy restart: unless-stopped command: - serve networks: - proxy volumes: - ./data:/var/cache/ntfy - ./etc:/etc/ntfy environment: NTFY_BASE_URL: https://ntfy.vds.kzntsv.site NTFY_CACHE_FILE: /var/cache/ntfy/cache.db NTFY_AUTH_FILE: /var/cache/ntfy/auth.db NTFY_AUTH_DEFAULT_ACCESS: deny-all NTFY_BEHIND_PROXY: true NTFY_ATTACHMENT_CACHE_DIR: /var/cache/ntfy/attachments labels: - traefik.enable=true - traefik.http.routers.ntfy.rule=Host(`ntfy.vds.kzntsv.site`) - traefik.http.routers.ntfy.entrypoints=websecure - traefik.http.routers.ntfy.tls.certresolver=letsEncrypt - traefik.http.services.ntfy.loadbalancer.server.port=80 networks: proxy: external: true ``` После up — `docker exec ntfy ntfy user add --role=admin vitya` → задать пароль → грант access на топики. Publish from any script: ```bash curl -u "vitya:$NTFY_PASS" \ -H "Title: VDS backup completed" \ -H "Tags: white_check_mark" \ -H "Priority: default" \ -d "Size 12.5G, 18m12s, kreknin OK" \ https://ntfy.vds.kzntsv.site/vds-backup ``` Phone subscribes to `https://ntfy.vds.kzntsv.site/vds-backup` (с basic auth) — получает уведомление. ## Decisions log - **2026-05-20** — Image pinned `binwiederhier/ntfy:v2.11.0` (conservative, matches pin policy остальных стэков: traefik v2.11, portainer 2.21.5). - **2026-05-20** — Auth: `NTFY_AUTH_DEFAULT_ACCESS=deny-all` + admin user `vitya` (random hex16 password). Admin role гранит rw ко всем топикам автоматом — per-topic `ntfy access` calls returned `user vitya is an admin user, access control entries have no effect`. Не нужны явные grants; разлогиниться от admin позже если будет потребность в ограниченных read-only users. - **2026-05-20** — Topics: `vds-backup` + `vds-ops` (минимум для текущих нужд). Расширение бесплатное (admin → rw to all). - **2026-05-20** — TLS через traefik LE http-01 (issuer R12, valid Aug 18 2026); ntfy listens HTTP `:80` внутри `proxy` network, `NTFY_BEHIND_PROXY=true` чтобы trust X-Forwarded-* от traefik. - **2026-05-20** — Smoke green: anon publish/subscribe → 403, authed publish vds-backup+vds-ops → 200 с JSON id/ts. - **2026-05-20** — User feedback: random hex password неудобен для ввода в phone app. Меняю на `Pryakhin9` (consistency с TRAEFIK_DASHBOARD_PASS). Trade-off: меньше entropy, но scope ограничен (admin role, single user, push-сервис без чувствительных данных, can rotate если threat model изменится). Lesson — для human-input creds (phone/web UI) использовать память user'а, не random hex; random hex оставить для script-only (DB, API keys). - **2026-05-20** — Topics renamed `backup` → `vds-backup`, `ops` → `vds-ops` (consistency с VDS scope, упрощает filtering если позже добавятся nas-* или kreknin-* топики). - **2026-05-20** — Phone-verified: Android ntfy app subscription → оба push'а пришли в течение секунд. End-to-end complete. ## Completed steps - [x] `/opt/stacks/ntfy/{data}` создано (owner vitya:vitya) - [x] `docker-compose.yml` написан с traefik labels (Host, websecure, letsEncrypt, port 80) - [x] `docker compose up -d` → container healthy, listening :80 - [x] Admin user `vitya` добавлен через `printf "%s\n%s\n" "$PASS" "$PASS" | docker exec -i ntfy ntfy user add --role=admin vitya` - [x] LE cert issued via http-01, R12, valid 2026-05-20 → 2026-08-18 - [x] Smoke test (publish/subscribe/anon-deny) green - [x] Creds saved: `/opt/stacks/ntfy/.env` (chmod 600) + Windows `~/projects/.common/secrets/vds-kzntsv.env` - [x] Password rotated random-hex → `Pryakhin9` per user request (ntfy user change-pass) - [x] Topics renamed `backup`/`ops` → `vds-backup`/`vds-ops` - [x] Phone-verified (Android ntfy app, screenshot 22:54 MSK) ## Notes - Триггер: после `[[vds-kzntsv-bootstrap]]` Phase 3 и до или одновременно с `[[vds-backup-rsync-kreknin]]` (backup pipeline хочет ntfy для статус-уведомлений). - Интегрируется с `[[vds-backup-rsync-kreknin]]` — backup script публикует через ntfy + дублируется email'ом для надёжности. - **Android setup hint:** ntfy app → Add subscription → Topic `vds-backup` (или `vds-ops`) → Server `https://ntfy.vds.kzntsv.site` → enable «Use auth» → user `vitya` / password из vds-kzntsv.env. - Cert renewal — automatic via traefik (letsEncrypt resolver), нет дополнительных шагов.