Gotify Push Notifications: Setup a Private Alert Server with Docker

Learn how to set up Gotify on a VPS using Docker Compose. Build your own notification server to receive alerts in real-time.

Gotify Push Notifications: Setup a Private Alert Server with Docker

Self-hosting your own push notification service guarantees data privacy, eliminates dependence on third-party APIs (like Firebase Cloud Messaging or Apple Push Notification service), and removes rate-limiting constraints. Gotify is a self-hosted, lightweight notification server written in Go that allows you to send and receive messages in real time via a simple REST API and a persistent WebSocket connection.

This guide provides a comprehensive, production-ready walk-through to self-host Gotify on a virtual private server (VPS) using Docker Compose. We will focus on optimal environment variables, secure reverse proxy configurations for handling WebSockets, REST API authentication, and system hardening.

Core Prerequisites and Network Architecture

To follow this guide, you will need: - A VPS running a Linux distribution (such as Debian or Ubuntu) with Docker and the Docker Compose plugin installed. - A registered domain name or subdomain (e.g., gotify.example.com) pointing to your VPS IP address. - Port 80 and Port 443 open on your VPS firewall (e.g., UFW or iptables) to allow Web traffic.

The architecture consists of an incoming request hitting a reverse proxy (like Nginx, Traefik, or Caddy) over HTTPS, which terminates SSL and forwards requests to the Gotify container. Clients (such as Android devices or web browsers) maintain a persistent TCP connection via WebSockets to receive instant push alerts.

graph TD
    Client[Android App / Browser Client] -- "1. Persistent WebSocket (HTTPS/WSS)" --> Proxy[Reverse Proxy (Nginx/Traefik)]
    Server[Alert Source (Cron, Script, App)] -- "2. POST Notification (HTTPS REST API)" --> Proxy
    Proxy -- "3. Forward Port 8080" --> Gotify[Gotify Docker Container]

1. Gotify Docker Compose Configuration

Create a dedicated directory for your Gotify configuration to maintain clean storage organization:

mkdir -p ~/gotify && cd ~/gotify

Inside this directory, create the docker-compose.yml file. This configuration uses the official Gotify image, pins the version to prevent unexpected breaking changes, establishes a persistent volume for SQLite database storage, sets up a dedicated network, and implements a Docker health check.

Create the file docker-compose.yml:

version: "3.8"

services:
  gotify:
    image: gotify/server:2.4.0
    container_name: gotify
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    environment:
      - TZ=UTC
      # Default admin credentials (change these on first login)
      - GOTIFY_DEFAULTUSER_NAME=admin
      - GOTIFY_DEFAULTUSER_PASS=ChangeThisSecurePassword123!
      # Registration allows new users to register via the Web UI (false for private servers)
      - GOTIFY_REGISTRATION=false
      # Configures the password strength requirements
      - GOTIFY_PASS_STRENGTH=10
      # Upload limits (e.g., for application icons)
      - GOTIFY_UPLOADEDIMAGESSIZE=10485760 # 10MB in bytes
    volumes:
      - ./data:/app/data
    networks:
      - gotify_net
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

networks:
  gotify_net:
    driver: bridge

Key Parameters Explained

  • ports Binding (127.0.0.1:8080:80): By binding the container port 80 to loopback address 127.0.0.1 on port 8080, we prevent exposing Gotify directly to the public internet on HTTP. Only our local reverse proxy will be able to access it.
  • GOTIFY_REGISTRATION=false: Disables public sign-ups on your server. This is critical for private alert systems to prevent unauthorized access.
  • volumes Mount: Maps the container's /app/data directory to the host's ./data directory. Gotify uses SQLite by default; this ensures that your database, uploaded images, and configuration settings persist when the container is recreated.

Run the container in detached mode:

docker compose up -d

2. Reverse Proxy and WebSocket Configuration

Because Gotify relies on WebSockets for real-time communication, a standard reverse proxy setup will drop connections after brief periods of inactivity. The reverse proxy must be configured to pass the Upgrade and Connection headers properly, and keep-alive timeouts must be extended.

Below are configurations for three common reverse proxies: Nginx, Traefik, and Caddy. Choose the one that matches your stack.

Option A: Nginx Configuration

If you run Nginx on your VPS host, use this server block. It handles SSL termination via Let's Encrypt (using certbot) and sets up WebSocket routing.

Create or edit your site config file (e.g., /etc/nginx/sites-available/gotify.example.com):

upstream gotify_backend {
    server 127.0.0.1:8080;
    keepalive 32;
}

server {
    listen 80;
    server_name gotify.example.com;

    # Redirect all HTTP requests to HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name gotify.example.com;

    # SSL configurations (replace with path to your certificates)
    ssl_certificate /etc/letsencrypt/live/gotify.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/gotify.example.com/privkey.pem;

    # Modern SSL configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    # Security Headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;
    add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;

    # Proxy settings for Gotify
    location / {
        proxy_pass http://gotify_backend;

        # Standard HTTP headers
        proxy_set_header Host $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;

        # WebSocket support
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";

        # Buffer adjustments
        proxy_buffering off;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }
}

Why are these settings needed?

  • proxy_set_header Upgrade $http_upgrade & Connection "Upgrade": Tells Nginx to upgrade the HTTP connection to a binary WebSocket tunnel. Without these, the initial handshake fails and Gotify clients will fall back to periodic API polling.
  • proxy_read_timeout 86400s: Sets the timeout for reading a response from the proxied server to 24 hours. Default timeouts are usually 60 seconds, which would terminate inactive WebSockets continuously, triggering frequent client reconnect loops.
  • proxy_buffering off: Prevents Nginx from buffering the request payload, which ensures that messages are delivered with zero-latency.

Option B: Traefik (Docker Provider)

If you are using Traefik as an edge router in your Docker environment, you can configure Gotify entirely through labels in docker-compose.yml.

Update your docker-compose.yml to include the following configuration:

version: "3.8"

services:
  gotify:
    image: gotify/server:2.4.0
    container_name: gotify
    restart: unless-stopped
    environment:
      - TZ=UTC
      - GOTIFY_DEFAULTUSER_NAME=admin
      - GOTIFY_DEFAULTUSER_PASS=ChangeThisSecurePassword123!
      - GOTIFY_REGISTRATION=false
    volumes:
      - ./data:/app/data
    networks:
      - traefik_net
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.gotify.rule=Host(`gotify.example.com`)"
      - "traefik.http.routers.gotify.entrypoints=websecure"
      - "traefik.http.routers.gotify.tls=true"
      - "traefik.http.routers.gotify.tls.certresolver=myresolver"
      - "traefik.http.services.gotify.loadbalancer.server.port=80"

networks:
  traefik_net:
    external: true

Option C: Caddy File Configuration

Caddy simplifies SSL generation and automatically handles WebSocket upgrades. Below is a production Caddyfile block:

gotify.example.com {
    reverse_proxy 127.0.0.1:8080 {
        # Caddy automatically forwards WebSocket Upgrade headers.
        # We can increase the read timeout to keep WebSockets alive.
        header_up Host {host}
        header_up X-Real-IP {remote}
    }

    header {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        Strict-Transport-Security "max-age=31536000;"
    }
}

3. Secure REST API Notification Tokens

Gotify uses two distinct types of tokens for security separation: 1. Application Tokens: Used by external scripts, tools, or applications to send messages to Gotify. These are write-only tokens; they cannot read other applications' messages. 2. Client Tokens: Used by clients (e.g., the Gotify Web App or Android App) to subscribe to WebSockets and receive incoming messages from Gotify.

Generating an Application Token

  1. Log into your Gotify UI (https://gotify.example.com) using your administrator account.
  2. Navigate to Apps in the top navigation bar.
  3. Click Create Application. Give it a descriptive name (e.g., Backup Server Alerts) and optionally upload an icon.
  4. Copy the generated API Token (e.g., A8d9K2nSLx93k2s). Keep this token confidential.

Sending Notifications via the REST API

Once you have an Application Token, you can send notification payloads using HTTP POST requests.

To avoid exposing your API token in network access logs (which often capture URL query parameters), pass the token in the X-Gotify-Key header:

curl -X POST "https://gotify.example.com/message" \
     -H "X-Gotify-Key: A8d9K2nSLx93k2s" \
     -H "Content-Type: application/json" \
     -d '{
       "title": "Database Backup Status",
       "message": "The daily database backup completed successfully. Total backup size: 1.2GB.",
       "priority": 5
     }'

Method B: Using Query Parameters (Quick Tests)

For simple integrations or devices that cannot set custom headers, you can pass the token as a query parameter:

curl -X POST "https://gotify.example.com/message?token=A8d9K2nSLx93k2s" \
     -H "Content-Type: application/json" \
     -d '{
       "title": "System Alert",
       "message": "High CPU utilization detected on Web01.",
       "priority": 8
     }'

Formatting Notification Payloads

Gotify supports advanced layout options within the JSON request payload:

{
  "title": "Disk Warning",
  "message": "Disk usage is currently at **87%**.\n\n* Mountpoint: `/var` \n* Available: `12GB`",
  "priority": 7,
  "extras": {
    "client::notification": {
      "click": {
        "url": "https://dashboard.example.com/metrics"
      }
    },
    "client::display": {
      "contentType": "text/markdown"
    }
  }
}

Advanced Payload Customization:

  • Priority Mapping:
  • 0 to 3: Low priority (silent notification in Android UI).
  • 4 to 7: Normal priority (standard sound/vibration).
  • 8 to 10: High priority (bypasses Do Not Disturb/shows heads-up display depending on Android app configuration).
  • client::display: Setting contentType to text/markdown enables rich text rendering of the message body in the Gotify UI.
  • client::notification: Specifying a click.url makes the notification clickable on mobile clients, opening the target URL directly when tapped.

4. Client Configuration and Persistent WebSockets

To receive notifications on your mobile device:

  1. Install the Gotify Android app (available on F-Droid or GitHub Releases).
  2. Enter your server URL: https://gotify.example.com.
  3. Log in with your admin credentials. A Client Token is automatically negotiated and saved.
  4. Keep the connection alive:
  5. Go to your Android settings -> Apps -> Gotify -> Battery Optimization -> Set to Don't Optimize or Unrestricted.
  6. Within the Gotify Android app, go to Settings and check Keep-Alive Interval. If your reverse proxy connection closes despite the settings in Section 2, lower the keep-alive interval to 5 minutes or 10 minutes to send heartbeat packets.

5. Maintenance and Backup

To update your Gotify instance to a newer version:

cd ~/gotify
docker compose pull
docker compose up -d

Database Backups

Because Gotify stores all data in a single SQLite database file, creating backups is straightforward. Back up the host volume directory securely:

# Safely copy SQLite database without locking issues
sqlite3 ~/gotify/data/gotify.db ".backup '/backup/gotify_$(date +%F).db'"

This ensures you can restore your alerts, user permissions, and applications instantly in the event of hardware failure.