Deploying Gotify Push Notification Server with Docker

Deploying Gotify Push Notification Server with Docker

What You’ll Need

  • Hetzner VPS or DigitalOcean running Ubuntu 22.04 LTS
  • Namecheap for managing your custom domain and DNS records
  • Docker Engine and Docker Compose V2 installed on your server
  • n8n Cloud or self-hosted n8n for triggering automated alerts

Table of Contents

1. Server Preparation and Firewall Configuration

When building an automated operational workflow, fast push notifications are non negotiable. External services like Pushover or Pushbullet work well, but they introduce third party dependencies, rate limits, and recurring costs. Gotify is a self-hosted, open source push notification server that receives messages via a simple REST API and broadcasts them to Android clients or web UI subscribers in real time over WebSockets.

To host Gotify reliably, I recommend starting with an active cloud instance like a Hetzner VPS or a droplet from DigitalOcean. A single vCPU with 1GB of RAM is more than enough to handle millions of monthly notifications.

Before running any containerized services, ensure your underlying host operating system is locked down. If you have not done so already, complete our guide on Hardening SSH Server Security on Ubuntu VPS to restrict root access, enforce key-based authentication, and install Fail2ban.

Once connected to your server, update system packages and configure the Uncomplicated Firewall (UFW) to allow standard HTTP, HTTPS, and SSH traffic.

sudo apt update && sudo apt upgrade -y
sudo apt install ufw docker.io docker-compose-v2 -y
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Confirm that the UFW service is active and Docker is running:

sudo systemctl status docker --no-pager
sudo ufw status verbose

💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.

2. Deploying Gotify with Docker Compose

We will deploy Gotify inside an isolated Docker container, binding its web interface to localhost so that it remains accessible only through a reverse proxy. This architecture keeps direct HTTP access closed to the public internet while securing traffic with automatic TLS certificates.

Create a dedicated application directory at /opt/gotify and establish a volume mapping directory structure for persistent database storage and configuration options.

sudo mkdir -p /opt/gotify/gotify-data
cd /opt/gotify

Create a new configuration file named docker-compose.yml using your preferred text editor. We will set environment variables for default credentials, memory limits, and process timezones directly inside this file.

version: '3.8'

services:
  gotify:
    image: gotify/server:latest
    container_name: gotify
    restart: always
    ports:
      - "127.0.0.1:8080:80"
    environment:
      - GOTIFY_SERVER_PORT=80
      - GOTIFY_SERVER_KEEPCONTAINERFRAMES=false
      - GOTIFY_DATABASE_DIALECT=sqlite3
      - GOTIFY_DATABASE_CONNECTION=data/gotify.db
      - GOTIFY_DEFAULTUSER_NAME=admin
      - GOTIFY_DEFAULTUSER_PASS=ChangeThisSecurePassword123!
      - GOTIFY_PASSWORDBRUTEFORCEDELAY=500ms
      - TZ=UTC
    volumes:
      - ./gotify-data:/app/data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80/health"]
      interval: 30s
      timeout: 5s
      retries: 3

Now bring up the Gotify stack using Docker Compose:

sudo docker compose up -d

Verify that the container initialized successfully and is running on host port 8080:

sudo docker compose ps
curl http://127.0.0.1:8080/health

The output of the cURL command should return a {"health":"green"} JSON response.

3. Setting Up Caddy Reverse Proxy with Automatic SSL

Gotify relies heavily on persistent WebSocket connections for real time communication with client devices. Caddy is an ideal reverse proxy because it handles Let’s Encrypt SSL certificates automatically and proxies WebSockets without requiring complex custom header configurations.

First, navigate to your domain DNS provider, such as Namecheap, and create an A record pointing your domain or subdomain (for example push.yourdomain.com) directly to your VPS public IPv4 address.

Install Caddy on your Ubuntu host:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy -y

Now open /etc/caddy/Caddyfile and replace its existing content with the following reverse proxy block. Make sure to replace push.yourdomain.com with your actual domain name.

push.yourdomain.com {
    encode gzip zstd

    reverse_proxy 127.0.0.1:8080 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }

    log {
        output file /var/log/caddy/gotify_access.log
    }
}

Validate your Caddy syntax and restart the service to initiate automated SSL provision:

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl restart caddy

You can now open https://push.yourdomain.com in your web browser. Log in using the default credentials specified in your docker-compose.yml file (admin and ChangeThisSecurePassword123!). Navigate immediately to the Users tab inside the Gotify web interface and update the administrator password to a long, randomly generated secret.

4. Sending Push Notifications Programmatically

Gotify uses token-based authentication for both client listening and application publishing. To publish alerts, you must create an Application inside the Gotify UI.

  1. Log into your Gotify dashboard.
  2. Click Apps in the top navigation menu.
  3. Click Create Application, enter a name like Alert Automation, and click Create.
  4. Copy the generated application token (which begins with an A prefix, such as A1a2B3c4D5e6F7g).

Let’s test publishing a notification using various client implementations.

Bash and cURL Implementation

Here is a robust, production ready shell script that posts high-priority security notifications directly to your Gotify server.

#!/usr/bin/env bash

set -euo pipefail

GOTIFY_URL="https://push.yourdomain.com"
GOTIFY_TOKEN="A1a2B3c4D5e6F7g"

send_gotify_notification() {
    local title="$1"
    local message="$2"
    local priority="$3"

    local response
    response=$(curl -s -o /dev/null -w "%{http_code}" \
        -X POST "${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}" \
        -H "Content-Type: application/json" \
        -d "{
            \"title\": \"${title}\",
            \"message\": \"${message}\",
            \"priority\": ${priority},
            \"extras\": {
                \"client::display\": {
                    \"contentType\": \"text/markdown\"
                }
            }
        }")

    if [ "$response" -eq 200 ]; then
        echo "Notification delivered successfully."
    else
        echo "Failed to send notification. HTTP status code: ${response}"
        exit 1
    fi
}

send_gotify_notification "System Alert" "Backup job completed on server **prod-db-01**." 8

Python Implementation

For background backend workers, a Python script offers strong error handling capabilities. Save this file as send_alert.py:

import sys
import requests

GOTIFY_URL = "https://push.yourdomain.com"
APP_TOKEN = "A1a2B3c4D5e6F7g"

def push_message(title: str, message: str, priority: int = 5) -> bool:
    endpoint = f"{GOTIFY_URL}/message"
    params = {"token": APP_TOKEN}
    payload = {
        "title": title,
        "message": message,
        "priority": priority,
        "extras": {
            "client::notification": {
                "click": {"url": "https://push.yourdomain.com"}
            }
        }
    }

    try:
        response = requests.post(endpoint, params=params, json=payload, timeout=10)
        response.raise_for_status()
        print(f"Message sent! ID: {response.json().get('id')}")
        return True
    except requests.exceptions.RequestException as err:
        print(f"Error publishing notification: {err}", file=sys.stderr)
        return False

if __name__ == "__main__":
    push_message(
        title="Database CPU Spike",
        message="CPU utilization exceeded 92% for 5 consecutive minutes.",
        priority=10
    )

Node.js Implementation

When implementing stateful authentication workflows in Node.js apps, securing token handling is paramount. If you are building administrative backends, refer to our complete guide on Implementing JWT Refresh Token Rotation In Express to complement your notification delivery layer.

Here is a zero-dependency Native Fetch implementation for Node.js 18 and newer:

const GOTIFY_URL = "https://push.yourdomain.com";
const APP_TOKEN = "A1a2B3c4D5e6F7g";

async function sendPushNotification(title, message, priority = 5) {
  const url = new URL("/message", GOTIFY_URL);
  url.searchParams.append("token", APP_TOKEN);

  const payload = {
    title: title,
    message: message,
    priority: priority
  };

  try {
    const response = await fetch(url.toString(), {
      method: "POST",
      headers: {
        "Content-Type": "json"
      },
      body: JSON.stringify(payload)
    });

    if (!response.ok) {
      throw new Error(`Server returned status code ${response.status}`);
    }

    const data = await response.json();
    console.log(`Notification sent successfully. Message ID: ${data.id}`);
  } catch (error) {
    console.error(`Failed to push notification: ${error.message}`);
    process.exit(1);
  }
}

sendPushNotification("Payment Gateway Alert", "Failed charge attempt recorded for Account #4091", 7);

5. Advanced Integration and Event Deduplication

When hooking Gotify into high-frequency distributed monitoring systems or webhook processors like n8n Cloud, alert storms can quickly overwhelm your Android device or mobile client. If a failing microservice retries every 500 milliseconds, your push service might attempt to dispatch thousands of redundant alerts per minute.

To resolve this issue, apply robust event throttling patterns. Read our detailed architectural analysis on Handling Webhook Event Deduplication in Distributed Systems to learn how Redis cache layers and sliding window locks prevent duplicate processing.

Below is an updated workflow pattern that demonstrates how to send an HTTP alert from an n8n pipeline into your deployed Gotify instance:

  1. Add an HTTP Request node into your workflow canvas.
  2. Set Method to POST.
  3. Set URL to https://push.yourdomain.com/message.
  4. Add Query Parameter: Key token, Value A1a2B3c4D5e6F7g.
  5. Set Body Content Type to JSON.
  6. Define JSON Parameters using expression values dynamically retrieved from incoming Webhook payloads:
{
  "title": "Workflow Failure: {{$json.workflow.name}}",
  "message": "Execution ID {{$json.execution.id}} failed on node {{$json.node.name}}. Error: {{$json.error.message}}",
  "priority": 8
}

By leveraging custom priorities (ranging from 1 for lowest up to 10 for critical alerts), Gotify allows you to customize Android notification channels. Priority 10 messages can bypass Do Not Disturb rules on connected clients, making sure critical infrastructure failures wake you up while routine background task notifications remain silent.

Getting Started

To spin up your own real time self-hosted alert platform today:

  1. Provision an affordable server instance on Hetzner VPS or DigitalOcean.
  2. Register a dedicated alert subdomain through Namecheap.
  3. Execute the Docker Compose deployment scripts detailed above.
  4. Download the Gotify client application from Google Play or F-Droid, scan your server’s QR code, and start capturing events directly from your automated systems or n8n Cloud pipelines.

Outsource Your Automation

Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr, mention SYS3-HUGO for priority. Or DM at chasebot.online.

Want to automate this yourself?

Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.

Want this engine running on your own VPS?

This blog publishes itself — daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.

Get System 3
system online