ntfy Notification Server: Send Push Alerts to Your Devices with Docker

Deploy ntfy using Docker Compose. Send push notifications to your phone or desktop via simple HTTP requests from scripts and backend servers.

Self-hosting ntfy (pronounced notify) allows you to send push notifications to your desktop or mobile devices via simple HTTP POST/PUT requests. This guide covers a production-ready deployment using Docker Compose, including persistent storage, reverse proxy integration, access control, and rate limiting.

1. Directory Structure

Create a dedicated directory on your VPS to store the configuration, cache, and data files.

mkdir -p /opt/ntfy/config
cd /opt/ntfy
touch config/server.yml
touch docker-compose.yml

2. Docker Compose Configuration

Create the docker-compose.yml file to run the ntfy service. This configuration runs the container as a non-root user (UID 1000) for security, maps the config and data directories, and ensures the container restarts automatically.

version: '3.8'

services:
  ntfy:
    image: binwiederhier/ntfy:latest
    container_name: ntfy
    command: serve
    environment:
      - TZ=UTC
    volumes:
      - ./config:/etc/ntfy
      - ntfy-data:/var/cache/ntfy
    ports:
      - "127.0.0.1:8080:80"
    user: "1000:1000"
    restart: unless-stopped

volumes:
  ntfy-data:
    driver: local

3. ntfy Server Configuration

Configure /opt/ntfy/config/server.yml. Set your VPS domain and restrict anonymous usage.

# Hostname & Port
base-url: "https://ntfy.example.com"
listen-http: ":80"

# Trust reverse proxy headers (X-Forwarded-For)
behind-proxy: true

# Attachment Settings (Optional)
attachment-cache-dir: "/var/cache/ntfy/attachments"
attachment-limit: "15M"
attachment-expiry: "24h"

# Database and Cache
cache-file: "/var/cache/ntfy/cache.db"
auth-file: "/var/cache/ntfy/user.db"

# Default Permissions (Restrict anonymous access)
auth-default-access: "deny-all"

# iOS Instant Push (Upstream)
# iOS restricts background WebSockets. Use the official upstream server to proxy push notifications.
upstream-base-url: "https://ntfy.sh"

# Rate Limits (Mitigate DDoS/Abuse)
visitor-request-limit-burst: 100
visitor-request-limit-replenish: "5s"
visitor-attachment-total-daily-limit: "500M"

Note: Ensure the local directories mapped in volumes have ownership set to UID/GID 1000:1000 to prevent write permission issues.

chown -R 1000:1000 /opt/ntfy

4. Reverse Proxy Setup (WebSockets & SSE)

ntfy relies on long-lived HTTP connections (Server-Sent Events) and WebSockets for instant delivery. A standard reverse proxy configuration will drop these connections unless specifically configured.

Option A: Nginx

Add this configuration inside your Nginx server block:

server {
    listen 80;
    server_name ntfy.example.com;
    return 301 https://$host$request_uri;
}

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

    ssl_certificate /etc/letsencrypt/live/ntfy.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ntfy.example.com/privkey.pem;

    client_max_body_size 15M; # Must match attachment-limit

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;

        # WebSocket & Long-polling support
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Proxy "";

        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Ssl on;

        # Disable buffering for instant streaming (SSE)
        proxy_buffering off;
        proxy_read_timeout 24h;
    }
}

Option B: Caddy

Caddy supports WebSockets automatically without extra configuration:

ntfy.example.com {
    reverse_proxy 127.0.0.1:8080 {
        header_up X-Forwarded-Ssl on
    }

    # Optional: Match nginx client_max_body_size
    request_body {
        max_size 15MB
    }
}

5. Access Control & Users

Since auth-default-access is set to deny-all, you must explicitly create users and configure their access control lists (ACLs).

First, start the container:

docker compose up -d

Create Users

Create your admin and regular accounts:

# Create admin user
docker exec -it ntfy ntfy user add --role=admin adminuser

# Create regular user
docker exec -it ntfy ntfy user add subscriber1

Configure Access Control Lists (ACLs)

Assign permissions to topics (wildcards are supported):

# Grant subscriber1 read/write access to "myalerts" topic
docker exec -it ntfy ntfy access subscriber1 myalerts rw

# Grant subscriber1 write-only access to "sensors-*" topics
docker exec -it ntfy ntfy access subscriber1 "sensors-*" write

# Check active permissions
docker exec -it ntfy ntfy access

6. Testing Notifications

Send a Simple Notification

Use curl to publish a message. Authenticate using Basic Auth:

curl \
  -u "subscriber1:password" \
  -d "Backup job completed successfully." \
  -H "Title: Backup Status" \
  -H "Priority: high" \
  -H "Tags: white_check_mark,backup" \
  https://ntfy.example.com/myalerts

Send with File Attachments

Upload a log file or image directly as an attachment:

curl \
  -u "subscriber1:password" \
  -T "/var/log/syslog" \
  -H "Filename: syslog.txt" \
  https://ntfy.example.com/myalerts

Subscribe to a Topic (Real-Time JSON Stream)

Monitor notifications in real-time from a server script:

curl -s -u "subscriber1:password" https://ntfy.example.com/myalerts/json