From 0e54bfa37b3a1ee2aeb4ea7405d73a330bea66bb Mon Sep 17 00:00:00 2001 From: junyufan <1016891528@qq.com> Date: Thu, 21 May 2026 19:40:29 +0800 Subject: [PATCH] feat(notifications): complete Notifications Stack with ntfy + Gotify + Apprise - Enhanced docker-compose.yml: added Gotify v2.6.1, TLS certresolver, SSO ForwardAuth middleware, auth default access config - Comprehensive README.md with setup guides, integration docs for Watchtower/Alertmanager/Gitea/Home Assistant, health check commands - notify.sh CLI script for sending notifications via ntfy/Gotify/Apprise - All image tags pinned to specific versions (no latest) - CN network compatible (Docker Hub images only) Implements #13 ($80 USDT bounty) Co-Authored-By: Claude Opus 4.7 --- scripts/notify.sh | 86 +++++++++++++ stacks/notifications/README.md | 158 ++++++++++++++++++++++++ stacks/notifications/docker-compose.yml | 35 ++++++ 3 files changed, 279 insertions(+) create mode 100644 scripts/notify.sh create mode 100644 stacks/notifications/README.md diff --git a/scripts/notify.sh b/scripts/notify.sh new file mode 100644 index 00000000..9a541669 --- /dev/null +++ b/scripts/notify.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# notify.sh — Send notifications via ntfy, Gotify, or Apprise +# Usage: +# ./scripts/notify.sh --service ntfy --topic alerts --title "Test" --message "Hello" +# ./scripts/notify.sh --service gotify --title "Test" --message "Hello" +# ./scripts/notify.sh --service apprise --title "Test" --message "Hello" + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="${SCRIPT_DIR}/../.env" + +# Load .env if present +if [[ -f "$ENV_FILE" ]]; then + set -a + # shellcheck disable=SC1090 + source "$ENV_FILE" + set +a +fi + +SERVICE="" +TOPIC="" +TITLE="Homelab Notification" +MESSAGE="" +NTFY_URL="https://ntfy.${DOMAIN:-localhost}" +GOTIFY_URL="https://gotify.${DOMAIN:-localhost}" +APPRISE_URL="https://apprise.${DOMAIN:-localhost}" + +usage() { + echo "Usage: $0 --service [--topic TOPIC] --title TITLE --message MESSAGE" + echo "" + echo "Options:" + echo " --service Notification service: ntfy, gotify, or apprise" + echo " --topic Topic (ntfy only, default: 'homelab')" + echo " --title Notification title" + echo " --message Notification body" + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --service) SERVICE="$2"; shift 2 ;; + --topic) TOPIC="$2"; shift 2 ;; + --title) TITLE="$2"; shift 2 ;; + --message) MESSAGE="$2"; shift 2 ;; + -h|--help) usage ;; + *) echo "Unknown option: $1"; usage ;; + esac +done + +[[ -z "$SERVICE" ]] && { echo "Error: --service is required"; usage; } +[[ -z "$MESSAGE" ]] && { echo "Error: --message is required"; usage; } + +send_ntfy() { + local topic="${TOPIC:-homelab}" + curl -s -X POST "${NTFY_URL}/${topic}" \ + -H "Title: ${TITLE}" \ + -H "Priority: default" \ + -d "${MESSAGE}" || echo "Warning: ntfy send failed (is the stack running?)" +} + +send_gotify() { + local token="${GOTIFY_TOKEN:-}" + if [[ -z "$token" ]]; then + echo "Error: GOTIFY_TOKEN not set. Get it from https://gotify.${DOMAIN:-localhost}" + exit 1 + fi + curl -s -X POST "${GOTIFY_URL}/message" \ + -H "X-Gotify-Key: ${token}" \ + -d "title=${TITLE}" \ + -d "message=${MESSAGE}" \ + -d "priority=5" || echo "Warning: Gotify send failed" +} + +send_apprise() { + curl -s -X POST "${APPRISE_URL}/notify" \ + -d "title=${TITLE}" \ + -d "body=${MESSAGE}" || echo "Warning: Apprise send failed" +} + +case "$SERVICE" in + ntfy) send_ntfy ;; + gotify) send_gotify ;; + apprise) send_apprise ;; + *) echo "Unknown service: $SERVICE. Use: ntfy, gotify, apprise"; exit 1 ;; +esac diff --git a/stacks/notifications/README.md b/stacks/notifications/README.md new file mode 100644 index 00000000..658b020a --- /dev/null +++ b/stacks/notifications/README.md @@ -0,0 +1,158 @@ +# Notifications Stack + +Unified notification hub for HomeLab Stack. Supports push (ntfy), self-hosted push (Gotify), and multi-channel relay (Apprise). + +## What's Included + +| Service | Version | URL | Purpose | +|---------|---------|-----|---------| +| ntfy | v2.11.0 | `ntfy.` | Push notification server (subscribe via browser/mobile) | +| Gotify | v2.6.1 | `gotify.` | Self-hosted push notification server with web UI | +| Apprise | v1.1.6 | `apprise.` | Multi-channel notification relay (email, Telegram, Discord, etc.) | + +## Architecture + +``` +Homelab Services + │ + ├──► ntfy. ── browser/mobile push (watchtower, alertmanager) + ├──► gotify. ── Android/web push (via Gotify app) + └──► apprise.── multi-channel relay (email, Slack, Telegram, Discord...) + + ▲ + │ notifications trigger + ┌───────┴────────┐ + │ Watchtower │ container update alerts + │ Alertmanager │ Prometheus alerts + │ Gitea │ repo push/PR events + │ Home Assistant │ automation alerts + └────────────────┘ +``` + +## Quick Start + +```bash +# From repo root +cp .env.example .env +# Edit .env — set DOMAIN and GOTIFY_PASSWORD + +# Start base stack first (required for Traefik + proxy network) +cd stacks/base && docker compose up -d + +# Start notifications +cd ../notifications +ln -sf ../../.env .env +docker compose up -d +``` + +## Configuration + +### Environment Variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `DOMAIN` | Yes | — | Base domain for all services | +| `TZ` | No | `Asia/Shanghai` | Timezone | +| `GOTIFY_PASSWORD` | Yes | — | Admin password for Gotify | +| `GOTIFY_REGISTRATION` | No | `false` | Allow user self-registration | +| `NTFY_AUTH_DEFAULT_ACCESS` | No | `deny-all` | Default access policy | + +### ntfy Setup + +1. Visit `https://ntfy.` +2. Create a user: `docker exec ntfy ntfy user add --role=admin admin` +3. Grant access: `docker exec ntfy ntfy access '*' admin read-write` +4. Subscribe to topics via browser or mobile app (Android/iOS) + +### Gotify Setup + +1. Visit `https://gotify.` +2. Login with admin / `` +3. Create applications to get API tokens +4. Send test: `curl -X POST "https://gotify./message" -d "title=Test&message=Hello&priority=5" -H "X-Gotify-Key: "` + +### Apprise Setup + +1. Visit `https://apprise.` +2. Add notification URLs (e.g., `mailto://user:pass@smtp.gmail.com`, `tgram://bottoken/ChatID`) +3. Use the API to send: `curl -X POST "https://apprise./notify" -d "title=Alert&body=Something happened"` + +## Integration with Other Stacks + +### Watchtower (Auto-update notifications) + +Add to `.env`: +```env +WATCHTOWER_NOTIFICATIONS=shoutrrr +WATCHTOWER_NOTIFICATION_URL=ntfy://ntfy.${DOMAIN}/watchtower?user=admin&pass=xxx +``` + +### Alertmanager (Prometheus alerts) + +In Alertmanager config (`stacks/monitoring/alertmanager.yml`): +```yaml +receivers: + - name: 'ntfy' + webhook_configs: + - url: 'http://ntfy:80/' + send_resolved: true +``` + +### Gitea (Repository events) + +In Gitea webhook settings, set URL to: +``` +https://ntfy./gitea?auth=user:pass +``` + +### Home Assistant + +In HA `configuration.yaml`: +```yaml +notify: + - name: ntfy + platform: rest + resource: https://ntfy.{{ domain }}/homeassistant + method: POST +``` + +## SSO Integration (Authentik) + +Both ntfy and Gotify are configured with Traefik ForwardAuth middleware for SSO via Authentik. To enable: + +1. Deploy the SSO stack (`stacks/sso/`) +2. Create OIDC providers in Authentik for ntfy and gotify +3. The `authentik-forwardauth@docker` middleware is automatically applied via labels + +## CN Network Adaptation + +If `CN_MODE=true` in `.env`, use the CN mirror script: + +```bash +# Pull images through CN-friendly mirrors +./scripts/setup-cn-mirrors.sh +``` + +Images available on Docker Hub (no ghcr.io dependency) — compatible with CN mirrors. + +## Health Check Verification + +```bash +# Check all services +docker exec ntfy curl -sf http://localhost:80/v1/health +docker exec gotify curl -sf http://localhost:80/health +docker exec apprise curl -sf http://localhost:8000/ + +# One-liner +docker compose ps --format "table {{.Name}}\t{{.Status}}" +``` + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| ntfy returns 401 | Create admin user and grant access (see setup above) | +| Gotify login fails | Check `GOTIFY_PASSWORD` in `.env` | +| Traefik 404 | Ensure base stack is running and `proxy` network exists | +| Can't send notifications | Check container logs: `docker compose logs ntfy` | +| CN image pull timeout | Set `CN_MODE=true` and run `setup-cn-mirrors.sh` | diff --git a/stacks/notifications/docker-compose.yml b/stacks/notifications/docker-compose.yml index 2fa0f189..c62a2b4d 100644 --- a/stacks/notifications/docker-compose.yml +++ b/stacks/notifications/docker-compose.yml @@ -10,13 +10,17 @@ services: - ntfy-cache:/var/cache/ntfy environment: - TZ=${TZ:-Asia/Shanghai} + - NTFY_AUTH_DEFAULT_ACCESS=${NTFY_AUTH_DEFAULT_ACCESS:-deny-all} command: serve labels: - traefik.enable=true - "traefik.http.routers.ntfy.rule=Host(`ntfy.${DOMAIN}`)" - traefik.http.routers.ntfy.entrypoints=websecure - traefik.http.routers.ntfy.tls=true + - traefik.http.routers.ntfy.tls.certresolver=letsencrypt - traefik.http.services.ntfy.loadbalancer.server.port=80 + # SSO ForwardAuth (optional — requires Authentik stack) + - traefik.http.routers.ntfy.middlewares=authentik-forwardauth@docker healthcheck: test: [CMD-SHELL, "curl -sf http://localhost:80/v1/health || exit 1"] interval: 30s @@ -24,6 +28,35 @@ services: retries: 3 start_period: 15s + gotify: + image: gotify/server:v2.6.1 + container_name: gotify + restart: unless-stopped + networks: + - proxy + volumes: + - gotify-data:/app/data + environment: + - TZ=${TZ:-Asia/Shanghai} + - GOTIFY_DEFAULTUSER_NAME=admin + - GOTIFY_DEFAULTUSER_PASS=${GOTIFY_PASSWORD:?GOTIFY_PASSWORD is required} + - GOTIFY_REGISTRATION=${GOTIFY_REGISTRATION:-false} + labels: + - traefik.enable=true + - "traefik.http.routers.gotify.rule=Host(`gotify.${DOMAIN}`)" + - traefik.http.routers.gotify.entrypoints=websecure + - traefik.http.routers.gotify.tls=true + - traefik.http.routers.gotify.tls.certresolver=letsencrypt + - traefik.http.services.gotify.loadbalancer.server.port=80 + # SSO ForwardAuth (optional — requires Authentik stack) + - traefik.http.routers.gotify.middlewares=authentik-forwardauth@docker + healthcheck: + test: [CMD-SHELL, "curl -sf http://localhost:80/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + apprise: image: caronc/apprise:v1.1.6 container_name: apprise @@ -39,6 +72,7 @@ services: - "traefik.http.routers.apprise.rule=Host(`apprise.${DOMAIN}`)" - traefik.http.routers.apprise.entrypoints=websecure - traefik.http.routers.apprise.tls=true + - traefik.http.routers.apprise.tls.certresolver=letsencrypt - traefik.http.services.apprise.loadbalancer.server.port=8000 healthcheck: test: [CMD-SHELL, "curl -sf http://localhost:8000/ || exit 1"] @@ -54,4 +88,5 @@ networks: volumes: ntfy-data: ntfy-cache: + gotify-data: apprise-config: