Self-Host Gotify: Real-Time Push Notifications 2026
TL;DR
Gotify (MIT, ~10K GitHub stars, Go) is a self-hosted push notification server with a web UI, Android app, and REST API. Unlike ntfy (topic-based pub/sub), Gotify uses an application/client model — create an app, get a token, send messages. Gotify keeps a persistent message history you can browse in the web UI or query via API. Pushover charges $5 one-time per platform; Gotify is free with no limits.
Key Takeaways
- Gotify: MIT, ~10K stars, Go — push notifications with persistent message history
- Application model: Create apps (senders) and clients (receivers) — organized, not anonymous topics
- Web UI: Browse message history, manage apps, configure settings in a clean interface
- Android app: Native app with WebSocket real-time delivery — no polling delay
- REST API: Simple token-based API for sending messages from any script or service
- vs ntfy: Gotify has persistent history and web UI; ntfy has simpler sending syntax and iOS app
Gotify vs ntfy vs Pushover
| Feature | Gotify | ntfy | Pushover |
|---|---|---|---|
| Sending model | App tokens | Topic URLs | API keys |
| Message history | Yes (persistent) | Brief cache | No |
| Web UI | Yes | No | No |
| iOS app | No (web only) | Yes | Yes |
| Android app | Yes (native) | Yes (native) | Yes |
| Self-hosted | Yes | Yes | No (cloud) |
| WebSocket | Yes | Yes | No |
| Price | Free | Free | $5 one-time |
Part 1: Docker Setup
# docker-compose.yml
services:
gotify:
image: gotify/server:latest
container_name: gotify
restart: unless-stopped
ports:
- "8080:80"
volumes:
- gotify_data:/app/data
environment:
GOTIFY_DEFAULTUSER_NAME: admin
GOTIFY_DEFAULTUSER_PASS: "${ADMIN_PASSWORD}"
GOTIFY_SERVER_PORT: 80
GOTIFY_SERVER_KEEPALIVEPERIODSECONDS: 0
GOTIFY_SERVER_LISTENADDR: ""
GOTIFY_SERVER_SSL_ENABLED: "false"
GOTIFY_DATABASE_DIALECT: sqlite3
GOTIFY_DATABASE_CONNECTION: "data/gotify.db"
GOTIFY_PASSSTRENGTH: 10
GOTIFY_UPLOADEDIMAGESDIR: "data/images"
GOTIFY_PLUGINSDIR: "data/plugins"
GOTIFY_REGISTRATION: "false" # Disable public signup
TZ: America/Los_Angeles
volumes:
gotify_data:
docker compose up -d
Visit http://your-server:8080 → log in with admin credentials.
Part 2: HTTPS with Caddy
gotify.yourdomain.com {
reverse_proxy localhost:8080
}
Part 3: Create Apps and Send Messages
Create an application
- Apps → Create Application
- Name:
Home Server,Monitoring,CI Pipeline - Description: optional
- → Create → copy the App Token
Send a notification
APP_TOKEN="your-app-token"
BASE="https://gotify.yourdomain.com"
# Simple message:
curl -X POST "$BASE/message" \
-H "X-Gotify-Key: $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Backup Complete",
"message": "nightly backup finished successfully",
"priority": 5
}'
# Or via form data (simpler):
curl -X POST "$BASE/message?token=$APP_TOKEN" \
-F "title=Backup Complete" \
-F "message=Finished at $(date)" \
-F "priority=5"
Priority levels
| Priority | Behavior (Android) |
|---|---|
| 0 | Silently update badge |
| 1-3 | Low — quiet notification |
| 4-7 | Normal — default notification |
| 8-10 | High — loud, stays visible |
Part 4: Android App
- Install Gotify Android from F-Droid or GitHub releases
- + Add server:
- URL:
https://gotify.yourdomain.com - Username + password
- URL:
- Real-time WebSocket connection — notifications arrive instantly
Client token vs App token
- App token: Used by senders to POST messages
- Client token: Used by the Android app to receive messages (auto-created when you log in)
Part 5: REST API
BASE="https://gotify.yourdomain.com"
# Authenticate (get client token):
curl -X POST "$BASE/client" \
-u admin:password \
-H "Content-Type: application/json" \
-d '{"name": "my-script"}'
# List all messages:
curl "$BASE/message" \
-u admin:password | jq '.messages[].message'
# Delete a message:
curl -X DELETE "$BASE/message/42" \
-u admin:password
# Delete all messages for an app:
curl -X DELETE "$BASE/application/APP_ID/message" \
-u admin:password
# List applications:
curl "$BASE/application" \
-u admin:password | jq '.[] | {id, name, token}'
# Real-time stream (WebSocket):
# Connect to: wss://gotify.yourdomain.com/stream?token=CLIENT_TOKEN
Part 6: WebSocket Real-Time Stream
Subscribe to real-time notifications via WebSocket:
// Browser JavaScript:
const ws = new WebSocket(
"wss://gotify.yourdomain.com/stream?token=CLIENT_TOKEN"
);
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log(`[${msg.appid}] ${msg.title}: ${msg.message}`);
// Show browser notification:
new Notification(msg.title, { body: msg.message });
};
# Python:
import websocket
import json
def on_message(ws, message):
msg = json.loads(message)
print(f"[{msg['appid']}] {msg['title']}: {msg['message']}")
ws = websocket.WebSocketApp(
"wss://gotify.yourdomain.com/stream?token=CLIENT_TOKEN",
on_message=on_message
)
ws.run_forever()
Part 7: Integrations
Uptime Kuma
- Uptime Kuma → Notifications → Add → Gotify
- Server URL:
https://gotify.yourdomain.com - Token: your app token
- Priority: 8
Grafana
# Grafana → Contact Points → Webhook
URL: https://gotify.yourdomain.com/message?token=APP_TOKEN
Method: POST
Content-Type: application/json
Body template:
{
"title": "Grafana Alert: {{ .GroupLabels.alertname }}",
"message": "{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}",
"priority": 8
}
Cron notifications
#!/bin/bash
# /usr/local/bin/cron-notify.sh
# Usage: cron-notify.sh "App token" "Job name" command [args...]
TOKEN="$1"
JOB="$2"
shift 2
OUTPUT=$("$@" 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
curl -s -X POST "https://gotify.yourdomain.com/message?token=$TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"Cron Failed: $JOB\",
\"message\": \"Exit code: $EXIT_CODE\n$OUTPUT\",
\"priority\": 8
}"
fi
Watchtower Docker updates
services:
watchtower:
image: containrrr/watchtower
environment:
WATCHTOWER_NOTIFICATIONS: gotify
WATCHTOWER_NOTIFICATION_GOTIFY_URL: "https://gotify.yourdomain.com/"
WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN: "your-app-token"
Home Assistant
# configuration.yaml
notify:
- platform: rest
name: gotify
resource: "https://gotify.yourdomain.com/message"
method: POST_JSON
headers:
X-Gotify-Key: "your-app-token"
message_param_name: message
title_param_name: title
data:
priority: 7
Part 8: Message Markdown
Gotify supports Markdown in message bodies — rendered in the web UI and Android app:
curl -X POST "$BASE/message?token=$APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Deploy Report",
"message": "## Deployment Successful\n\n**Version**: 2.3.1\n**Environment**: Production\n**Duration**: 45s\n\n```\nBuilt 3 services\nPushed to registry\nDeployed to prod\n```",
"priority": 5,
"extras": {
"client::display": {
"contentType": "text/markdown"
}
}
}'
Maintenance
# Update:
docker compose pull
docker compose up -d
# Backup (SQLite):
docker cp gotify:/app/data/gotify.db \
./gotify-backup-$(date +%Y%m%d).db
# Or full data directory:
tar -czf gotify-data-$(date +%Y%m%d).tar.gz \
$(docker volume inspect gotify_gotify_data --format '{{.Mountpoint}}')
# Check message counts by app:
docker exec gotify sqlite3 /app/data/gotify.db \
"SELECT a.name, COUNT(m.id) as msgs FROM application a
LEFT JOIN message m ON a.id = m.applicationid
GROUP BY a.id ORDER BY msgs DESC;"
# Logs:
docker compose logs -f gotify
See also: ntfy — simpler pub/sub alternative with native iOS app and no account required
See all open source productivity tools at OSSAlt.com/categories/productivity.