feat(gotify): add push notification server + SMTP email relay
- Gotify server (ghcr.io/gotify/server:2.6.1) in notifications namespace - SMTP emailer sidecar polls messages, forwards as email via msmtp - Ingress at gotify.riotpiao.com with WebSocket support - 1Gi Longhorn PVC for message persistence - ArgoCD Application (wave 8, auto-sync) - Secrets template for admin creds, SMTP config, tokens - README with setup guide: tokens, Forgejo webhooks, SMTP providers Enables PR created/merged email notifications from Forgejo.
This commit is contained in:
@@ -0,0 +1,107 @@
|
|||||||
|
# Gotify — Push Notifications + Email Relay
|
||||||
|
|
||||||
|
Self-hosted notification server with SMTP email forwarding sidecar.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Forgejo webhook ──POST──→ Gotify API (:80/message)
|
||||||
|
│
|
||||||
|
┌─────────┼─────────┐
|
||||||
|
▼ ▼
|
||||||
|
Push notification SMTP emailer sidecar
|
||||||
|
(mobile/desktop) (polls → sends email)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup (one-time, after first deploy)
|
||||||
|
|
||||||
|
### 1. Encrypt secrets before committing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Edit secrets.yaml with real values first, then:
|
||||||
|
sops -e -i k8s/apps/gotify/secrets.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Gotify app + client tokens
|
||||||
|
|
||||||
|
1. Login to `https://gotify.riotpiao.com` with admin creds
|
||||||
|
2. **Applications** → Create `forgejo` → copy **app token**
|
||||||
|
3. **Clients** → Create `smtp-emailer` → copy **client token**
|
||||||
|
4. Update `gotify-tokens` secret:
|
||||||
|
```bash
|
||||||
|
kubectl -n notifications create secret generic gotify-tokens \
|
||||||
|
--from-literal=app-token=<APP_TOKEN> \
|
||||||
|
--from-literal=client-token=<CLIENT_TOKEN> \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure Forgejo webhook
|
||||||
|
|
||||||
|
In each Forgejo repo → **Settings** → **Webhooks** → **Add Webhook** → **Gotify**:
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Target URL | `http://gotify.notifications.svc.cluster.local/message` |
|
||||||
|
| Token | The **app token** from step 2 |
|
||||||
|
| Events | Pull Request (Created, Merged, Closed) |
|
||||||
|
|
||||||
|
Or via API:
|
||||||
|
```bash
|
||||||
|
FORGEJO_TOKEN="<your-pat>"
|
||||||
|
APP_TOKEN="<gotify-app-token>"
|
||||||
|
|
||||||
|
curl -s -X POST "https://forgejo.riotpiao.com/api/v1/repos/rock/homelab/hooks" \
|
||||||
|
-H "Authorization: token $FORGEJO_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"type": "gotify",
|
||||||
|
"active": true,
|
||||||
|
"config": {
|
||||||
|
"content_type": "json",
|
||||||
|
"url": "http://gotify.notifications.svc.cluster.local/message?token='"$APP_TOKEN"'"
|
||||||
|
},
|
||||||
|
"events": ["pull_request", "pull_request_assign", "pull_request_review"],
|
||||||
|
"authorization_header": ""
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Add CoreDNS rewrite (if accessing via public hostname)
|
||||||
|
|
||||||
|
Only needed if Cloudflare Tunnel is used for gotify.riotpiao.com:
|
||||||
|
|
||||||
|
```
|
||||||
|
# terraform/files/coredns/Corefile — add rewrite:
|
||||||
|
rewrite name gotify.riotpiao.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Then: `cd terraform && terraform apply && cd .. && make apply-cp`
|
||||||
|
|
||||||
|
### 5. SMTP providers
|
||||||
|
|
||||||
|
| Provider | Host | Port | Notes |
|
||||||
|
|----------|------|------|-------|
|
||||||
|
| Gmail | smtp.gmail.com | 587 | Use App Password (2FA required) |
|
||||||
|
| Resend | smtp.resend.com | 587 | Free 100 emails/day |
|
||||||
|
| Sendgrid | smtp.sendgrid.net | 587 | Free 100 emails/day |
|
||||||
|
| Mailgun | smtp.mailgun.org | 587 | Free 5000/month |
|
||||||
|
|
||||||
|
## Notification priority levels
|
||||||
|
|
||||||
|
| Priority | Meaning | Email forwarded? |
|
||||||
|
|----------|---------|-----------------|
|
||||||
|
| 0-4 | Low (info) | No (below MIN_PRIORITY=5) |
|
||||||
|
| 5-7 | Normal (PR created) | Yes |
|
||||||
|
| 8-10 | High (PR merged, failures) | Yes |
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test push notification
|
||||||
|
APP_TOKEN="<app-token>"
|
||||||
|
curl -X POST "https://gotify.riotpiao.com/message?token=$APP_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"title":"Test","message":"Hello from homelab","priority":5}'
|
||||||
|
|
||||||
|
# Check email sidecar logs
|
||||||
|
kubectl -n notifications logs deployment/gotify -c smtp-emailer --tail=20
|
||||||
|
```
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
# Gotify — self-hosted push notification server + SMTP email relay.
|
||||||
|
# Forgejo webhooks → Gotify → push notifications + email forwarding.
|
||||||
|
# Runs on control plane (no GPU needed), lightweight.
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: gotify
|
||||||
|
namespace: notifications
|
||||||
|
labels:
|
||||||
|
app: gotify
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: gotify
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: gotify
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
# --- Gotify server ---
|
||||||
|
- name: gotify
|
||||||
|
image: ghcr.io/gotify/server:2.6.1
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: GOTIFY_DEFAULTUSER_NAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: gotify-admin
|
||||||
|
key: username
|
||||||
|
- name: GOTIFY_DEFAULTUSER_PASS
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: gotify-admin
|
||||||
|
key: password
|
||||||
|
- name: GOTIFY_SERVER_PORT
|
||||||
|
value: "80"
|
||||||
|
- name: GOTIFY_SERVER_KEEPALIVEPERIODSECONDS
|
||||||
|
value: "0"
|
||||||
|
- name: TZ
|
||||||
|
value: Asia/Tokyo
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 128Mi
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /app/data
|
||||||
|
name: data
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 80
|
||||||
|
periodSeconds: 30
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 80
|
||||||
|
periodSeconds: 10
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
|
||||||
|
# --- SMTP emailer sidecar ---
|
||||||
|
# Watches Gotify WebSocket stream, forwards messages as email.
|
||||||
|
# https://github.com/eternal-flame-AD/gotify-broadcast
|
||||||
|
- name: smtp-emailer
|
||||||
|
image: ghcr.io/gotify/server:2.6.1
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
# Wait for Gotify to be ready
|
||||||
|
until wget -qO- http://localhost:80/health >/dev/null 2>&1; do
|
||||||
|
echo "Waiting for Gotify..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "Gotify is ready, starting email relay..."
|
||||||
|
|
||||||
|
# Poll Gotify messages and forward via SMTP using msmtp
|
||||||
|
# Install msmtp for lightweight SMTP sending
|
||||||
|
apk add --no-cache msmtp curl jq
|
||||||
|
|
||||||
|
# Configure msmtp
|
||||||
|
cat > /tmp/msmtprc <<MSMTP
|
||||||
|
defaults
|
||||||
|
auth on
|
||||||
|
tls on
|
||||||
|
tls_trust_file /etc/ssl/certs/ca-certificates.crt
|
||||||
|
logfile /tmp/msmtp.log
|
||||||
|
|
||||||
|
account default
|
||||||
|
host ${SMTP_HOST}
|
||||||
|
port ${SMTP_PORT}
|
||||||
|
from ${SMTP_FROM}
|
||||||
|
user ${SMTP_USER}
|
||||||
|
password ${SMTP_PASS}
|
||||||
|
MSMTP
|
||||||
|
|
||||||
|
chmod 600 /tmp/msmtprc
|
||||||
|
|
||||||
|
# Track last seen message ID
|
||||||
|
LAST_ID=0
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
# Fetch messages since last ID
|
||||||
|
MESSAGES=$(curl -s -H "X-Gotify-Key: ${GOTIFY_CLIENT_TOKEN}" \
|
||||||
|
"http://localhost:80/message?since=${LAST_ID}&limit=10" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$MESSAGES" ]; then
|
||||||
|
echo "$MESSAGES" | jq -r '.messages[]? | @base64' | while read -r MSG; do
|
||||||
|
DECODED=$(echo "$MSG" | base64 -d)
|
||||||
|
ID=$(echo "$DECODED" | jq -r '.id')
|
||||||
|
TITLE=$(echo "$DECODED" | jq -r '.title // "Notification"')
|
||||||
|
BODY=$(echo "$DECODED" | jq -r '.message // ""')
|
||||||
|
PRIORITY=$(echo "$DECODED" | jq -r '.priority // 5')
|
||||||
|
APP=$(echo "$DECODED" | jq -r '.appid // 0')
|
||||||
|
DATE=$(echo "$DECODED" | jq -r '.date // ""')
|
||||||
|
|
||||||
|
# Only forward messages with priority >= configured threshold
|
||||||
|
if [ "$PRIORITY" -ge "${MIN_PRIORITY:-0}" ]; then
|
||||||
|
printf "Subject: [Gotify] %s\nFrom: %s\nTo: %s\nContent-Type: text/plain; charset=UTF-8\n\n%s\n\n---\nPriority: %s\nDate: %s" \
|
||||||
|
"$TITLE" "$SMTP_FROM" "$NOTIFY_EMAIL" "$BODY" "$PRIORITY" "$DATE" | \
|
||||||
|
msmtp -C /tmp/msmtprc "$NOTIFY_EMAIL" && \
|
||||||
|
echo "Email sent for message $ID: $TITLE" || \
|
||||||
|
echo "Failed to send email for message $ID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update last seen ID
|
||||||
|
if [ "$ID" -gt "$LAST_ID" ]; then
|
||||||
|
LAST_ID=$ID
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep ${POLL_INTERVAL:-30}
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
- name: GOTIFY_CLIENT_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: gotify-tokens
|
||||||
|
key: client-token
|
||||||
|
- name: SMTP_HOST
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: gotify-smtp
|
||||||
|
key: host
|
||||||
|
- name: SMTP_PORT
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: gotify-smtp
|
||||||
|
key: port
|
||||||
|
- name: SMTP_FROM
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: gotify-smtp
|
||||||
|
key: from
|
||||||
|
- name: SMTP_USER
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: gotify-smtp
|
||||||
|
key: user
|
||||||
|
- name: SMTP_PASS
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: gotify-smtp
|
||||||
|
key: password
|
||||||
|
- name: NOTIFY_EMAIL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: gotify-smtp
|
||||||
|
key: notify-email
|
||||||
|
- name: MIN_PRIORITY
|
||||||
|
value: "5"
|
||||||
|
- name: POLL_INTERVAL
|
||||||
|
value: "15"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 32Mi
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 64Mi
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: gotify-data
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: gotify
|
||||||
|
namespace: notifications
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
|
||||||
|
# WebSocket support for Gotify client connections
|
||||||
|
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
|
||||||
|
nginx.ingress.kubernetes.io/configuration-snippet: |
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: gotify.riotpiao.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: gotify
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- pvc.yaml
|
||||||
|
- secrets.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: notifications
|
||||||
|
labels:
|
||||||
|
kubernetes.io/metadata.name: notifications
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: gotify-data
|
||||||
|
namespace: notifications
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Gotify secrets — SOPS-encrypt before committing!
|
||||||
|
# sops -e -i k8s/apps/gotify/secrets.yaml
|
||||||
|
#
|
||||||
|
# After Gotify starts:
|
||||||
|
# 1. Login to gotify.riotpiao.com with admin creds below
|
||||||
|
# 2. Create an Application (e.g., "forgejo") → copy the app token
|
||||||
|
# 3. Create a Client → copy the client token
|
||||||
|
# 4. Update gotify-tokens secret with the client token
|
||||||
|
# 5. Configure Forgejo webhook (see README below)
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: gotify-admin
|
||||||
|
namespace: notifications
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
username: admin
|
||||||
|
password: CHANGE_ME_BEFORE_DEPLOY
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: gotify-tokens
|
||||||
|
namespace: notifications
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
# Create after first login:
|
||||||
|
# - App token: Gotify UI → Applications → Create → copy token
|
||||||
|
# - Client token: Gotify UI → Clients → Create → copy token
|
||||||
|
app-token: CHANGE_AFTER_FIRST_LOGIN
|
||||||
|
client-token: CHANGE_AFTER_FIRST_LOGIN
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: gotify-smtp
|
||||||
|
namespace: notifications
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
# SMTP config for email forwarding
|
||||||
|
# Gmail example (use App Password, not account password):
|
||||||
|
host: smtp.gmail.com
|
||||||
|
port: "587"
|
||||||
|
from: [email protected]
|
||||||
|
user: CHANGE_ME
|
||||||
|
password: CHANGE_ME
|
||||||
|
notify-email: [email protected]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: gotify
|
||||||
|
namespace: notifications
|
||||||
|
labels:
|
||||||
|
app: gotify
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: gotify
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: gotify
|
||||||
|
namespace: argocd
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: gotify
|
||||||
|
app.kubernetes.io/component: notifications
|
||||||
|
annotations:
|
||||||
|
argocd.argoproj.io/sync-wave: "8"
|
||||||
|
spec:
|
||||||
|
project: homelab
|
||||||
|
revisionHistoryLimit: 3
|
||||||
|
source:
|
||||||
|
repoURL: https://forgejo.riotpiao.com/rock/homelab.git
|
||||||
|
targetRevision: main
|
||||||
|
path: k8s/apps/gotify
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: notifications
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
|
retry:
|
||||||
|
limit: 5
|
||||||
|
backoff:
|
||||||
|
duration: 5s
|
||||||
|
factor: 2
|
||||||
|
maxDuration: 3m
|
||||||
Reference in New Issue
Block a user