Gotify is a self-hosted server for sending and receiving push notifications. Scripts and services send messages through a REST API using a per-application token, and clients (the web UI, the Android app or your own code) receive them in real time over a WebSocket. In this tutorial you will run Gotify with Docker Compose on Ubuntu 24.04, publish it over HTTPS with Nginx, create an application token, and send notifications from the command line and from a backup job.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user that has
sudoprivileges. - Docker Engine and the Docker Compose plugin installed from Docker's official repository.
- A domain or subdomain, such as
gotify.your_domain, with a DNS A record pointing to the server's public IP. - Nginx installed (
sudo apt install nginx) and UFW allowing HTTP and HTTPS (sudo ufw allow 'Nginx Full').
Gotify concepts
Gotify separates senders from receivers with two kinds of tokens:
| Object | Token prefix | Used by | Can do |
|---|---|---|---|
| Application | A | Scripts and services that send messages | Only send messages |
| Client | C | Web UI, Android app, custom listeners | Read messages and manage the account |
Every message belongs to one application, so you can see at a glance which system sent it and delete or mute that application's messages as a group.
Step 1 - Creating the Compose project
Create a directory for Gotify and its data:
sudo mkdir -p /opt/gotify/data
cd /opt/gotify
Store the initial admin password in an .env file so it does not end up in the Compose file. Replace your_strong_password with a long random password:
echo "GOTIFY_ADMIN_PASS=your_strong_password" | sudo tee .env > /dev/null
sudo chmod 600 .env
Create the Compose file:
sudo nano compose.yaml
services:
gotify:
image: gotify/server:latest
container_name: gotify
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
environment:
TZ: "Etc/UTC"
GOTIFY_DEFAULTUSER_NAME: "admin"
GOTIFY_DEFAULTUSER_PASS: "${GOTIFY_ADMIN_PASS}"
volumes:
- ./data:/app/data
The port is bound to 127.0.0.1 on purpose. Docker writes its own iptables rules, so a port published as 8080:80 would be reachable from the internet even if UFW blocks it. Binding to localhost leaves Nginx as the only way in.
GOTIFY_DEFAULTUSER_NAME and GOTIFY_DEFAULTUSER_PASS are only used on the first start, when the database is created. Changing them later has no effect; change the password in the web UI instead.
Step 2 - Starting Gotify
Start the container:
sudo docker compose up -d
Check that it is running:
sudo docker compose ps
NAME IMAGE COMMAND SERVICE STATUS PORTS
gotify gotify/server:latest "./gotify-app" gotify Up 10 seconds 127.0.0.1:8080->80/tcp
Query the health endpoint:
curl -s http://127.0.0.1:8080/health
{"health":"green","database":"green"}
The SQLite database, uploaded images and plugins are stored in /opt/gotify/data:
ls /opt/gotify/data
gotify.db images plugins
If the container keeps restarting, read its log with sudo docker compose logs gotify.
Step 3 - Publishing Gotify through Nginx with HTTPS
Clients keep a WebSocket open to /stream, so Nginx must pass the Upgrade headers and allow the connection to stay idle between Gotify's keepalive pings.
Create a server block:
sudo nano /etc/nginx/sites-available/gotify
server {
listen 80;
listen [::]:80;
server_name gotify.your_domain;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 1m;
proxy_send_timeout 1m;
proxy_read_timeout 1m;
}
}
Enable the site and reload Nginx:
sudo ln -s /etc/nginx/sites-available/gotify /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Request a certificate with Certbot. It adds the HTTPS listener and a redirect from HTTP:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d gotify.your_domain
Verify from your own computer:
curl -s https://gotify.your_domain/health
{"health":"green","database":"green"}
Step 4 - Securing the admin account
Open https://gotify.your_domain and log in as admin with the password from .env. Then:
- Click Settings in the top bar and set a new password. The value in
.envis no longer used after the first start, so you can delete the file afterwards withsudo rm /opt/gotify/.env. - If other people need access, open Users and create a separate, non-admin account for each person. Each user has their own applications and messages.
Step 5 - Creating an application token
Each system that sends notifications should have its own application.
In the web UI, open Apps, click Create Application, enter a name such as Server alerts and a description, and click Create. The new row shows the token (click the eye icon to reveal it). It starts with A.
You can do the same through the API with your user credentials. curl prompts for the password:
curl -s -u admin -H "Content-Type: application/json" \
-d '{"name": "Backups", "description": "Nightly backup results"}' \
https://gotify.your_domain/application
{"id":2,"token":"AoNiW1BV7qKk0PX","name":"Backups","description":"Nightly backup results","internal":false,"image":"static/defaultapp.png","defaultPriority":0,"lastUsed":null}
Keep the token value; this guide calls it your_app_token.
Step 6 - Sending messages
Send a message with the application token in the X-Gotify-Key header. Passing the token in a header rather than in the URL keeps it out of access logs:
curl -s -H "X-Gotify-Key: your_app_token" \
-F "title=Test from the server" \
-F "message=Gotify is working" \
-F "priority=5" \
https://gotify.your_domain/message
{"id":1,"appid":2,"message":"Gotify is working","title":"Test from the server","priority":5,"date":"2026-09-25T10:21:44.1Z"}
The message appears immediately in the web UI under the application. Allow browser notifications when the web UI asks, and it will also show a desktop notification while the tab is open.
The priority controls how the Android app alerts you:
| Priority | Android behavior |
|---|---|
| 0 | Stored, no notification |
| 1-3 | Notification without sound |
| 4-7 | Notification with sound |
| 8-10 | Notification with sound and vibration, shown as a pop-up |
For richer messages, send JSON and add extras. This example renders the body as Markdown and opens a dashboard when the notification is tapped:
curl -s -H "X-Gotify-Key: your_app_token" -H "Content-Type: application/json" \
-d '{
"title": "Disk usage warning",
"message": "**web-01**: `/` is at 91% capacity",
"priority": 8,
"extras": {
"client::display": {"contentType": "text/markdown"},
"client::notification": {"click": {"url": "https://grafana.your_domain"}}
}
}' \
https://gotify.your_domain/message
Step 7 - Receiving notifications on Android
Install the Gotify app from F-Droid, Google Play or the project's GitHub releases. Enter https://gotify.your_domain as the server URL and log in with your username and password. The app creates its own client token and keeps a WebSocket open in the background.
Send another test message with priority 8 and check that your phone vibrates. There is no official iOS app; iPhone users can use the web UI, or use ntfy, which supports iOS.
Step 8 - Notifying the result of a backup job
A small wrapper script that runs a job and reports its result is a good fit for Gotify. Create it:
sudo nano /usr/local/bin/backup-notify
#!/usr/bin/env bash
set -euo pipefail
GOTIFY_URL="https://gotify.your_domain/message"
GOTIFY_TOKEN_FILE="/etc/gotify-backup-token"
notify() {
curl -fsS -o /dev/null \
-H "X-Gotify-Key: $(cat "$GOTIFY_TOKEN_FILE")" \
-F "title=$1" \
-F "message=$2" \
-F "priority=$3" \
"$GOTIFY_URL"
}
if output=$("$@" 2>&1); then
notify "Backup OK on $(hostname)" "Finished at $(date '+%F %T')" 3
else
status=$?
notify "Backup FAILED on $(hostname)" "Exit code ${status}. Last lines: $(printf '%s\n' "$output" | tail -n 5)" 9
exit "$status"
fi
The script runs whatever command you pass to it, sends a low-priority message on success and a high-priority one with the last lines of output on failure, and keeps the original exit code.
Store the application token in a file only root can read, and make the script executable:
echo "your_app_token" | sudo tee /etc/gotify-backup-token > /dev/null
sudo chmod 600 /etc/gotify-backup-token
sudo chmod 755 /usr/local/bin/backup-notify
Test both outcomes with commands that succeed and fail:
sudo backup-notify true
sudo backup-notify false
You should receive a "Backup OK" message and a "Backup FAILED" message with exit code 1. Then schedule your real job through the wrapper, for example in /etc/cron.d/backup:
0 3 * * * root /usr/local/bin/backup-notify /usr/local/bin/backup.sh
Updating and backing up Gotify
To update to the latest release, pull the new image and recreate the container:
cd /opt/gotify
sudo docker compose pull
sudo docker compose up -d
Everything Gotify needs lives in /opt/gotify/data. Stop the container briefly and archive the directory to take a consistent backup:
sudo docker compose stop
sudo tar -czf /root/gotify-data-$(date +%F).tar.gz -C /opt/gotify data
sudo docker compose start
Troubleshooting
401 Unauthorized when sending a message. You used a client token (starting with C) or a deleted application's token. Messages must be sent with an application token (starting with A).
The web UI or Android app keeps reconnecting. The WebSocket is not passing through the proxy. Check that the Upgrade and Connection headers are present in the HTTPS server block created by Certbot, then run sudo nginx -t && sudo systemctl reload nginx.
Android notifications arrive late or not at all. The app needs a permanent connection. Disable battery optimization for Gotify in the Android settings and make sure the notification channels for your priorities are not muted.
Login fails with the password from .env. The database was created before you set it, or the password was changed in the UI. The environment variable is only read when gotify.db does not exist yet.
Conclusion
Gotify now runs in Docker on Ubuntu 24.04 behind Nginx with HTTPS, with a dedicated application token per sender, notifications on Android and the web, and a reusable wrapper that reports the result of any scheduled job. As next steps, create one application per service so you can mute or clean them up independently, connect Uptime Kuma or Grafana to Gotify for monitoring alerts, and include /opt/gotify/data in your regular off-site backups.
