Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions scripts/notify.sh
Original file line number Diff line number Diff line change
@@ -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 <ntfy|gotify|apprise> [--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
158 changes: 158 additions & 0 deletions stacks/notifications/README.md
Original file line number Diff line number Diff line change
@@ -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.<DOMAIN>` | Push notification server (subscribe via browser/mobile) |
| Gotify | v2.6.1 | `gotify.<DOMAIN>` | Self-hosted push notification server with web UI |
| Apprise | v1.1.6 | `apprise.<DOMAIN>` | Multi-channel notification relay (email, Telegram, Discord, etc.) |

## Architecture

```
Homelab Services
├──► ntfy.<DOMAIN> ── browser/mobile push (watchtower, alertmanager)
├──► gotify.<DOMAIN> ── Android/web push (via Gotify app)
└──► apprise.<DOMAIN>── 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.<DOMAIN>`
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.<DOMAIN>`
2. Login with admin / `<GOTIFY_PASSWORD>`
3. Create applications to get API tokens
4. Send test: `curl -X POST "https://gotify.<DOMAIN>/message" -d "title=Test&message=Hello&priority=5" -H "X-Gotify-Key: <token>"`

### Apprise Setup

1. Visit `https://apprise.<DOMAIN>`
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.<DOMAIN>/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.<DOMAIN>/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` |
35 changes: 35 additions & 0 deletions stacks/notifications/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,53 @@ 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
timeout: 10s
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
Expand All @@ -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"]
Expand All @@ -54,4 +88,5 @@ networks:
volumes:
ntfy-data:
ntfy-cache:
gotify-data:
apprise-config: