Skip to content

Self-hosting ntfy on Unraid

ntfy is a simple notification service that lets my homelab send push alerts to my phone, browser, or desktop using basic HTTP requests.

I set this up because I wanted one central place for homelab notifications instead of every app handling alerts differently. My goal was to have a small, reliable notification layer for things like backups, media requests, monitoring alerts, and general server events. This setup gives me a simple way to send alerts from almost anything that can make an HTTP request.

My setup at a glance

Component Purpose
Unraid Always-on Docker host
Docker Compose Runs and manages the ntfy container
ntfy Notification server
Appdata storage Persists config, cache, users, and tokens
Cloudflare Tunnel Optional public access without router port forwarding
ntfy mobile app Receives push notifications

What this guide includes

This is not a full click-by-click walkthrough. It is a public-safe overview of how I set up ntfy in my homelab.

Section Included
Docker Compose Yes
Sample server.yml Yes
Local testing Yes
Authentication approach Yes
Cloudflare Tunnel notes Yes
Real tokens, passwords, or private topics No

Folder layout

I keep ntfy in Unraid appdata so the container can be recreated without losing the important files.

/mnt/user/appdata/ntfy/
├── docker-compose.yml
├── server.yml
└── data/
    ├── cache.db
    ├── user.db
    └── attachments/
Path Purpose
docker-compose.yml Defines the ntfy container
server.yml Main ntfy server config
data/cache.db Notification cache database
data/user.db User/auth database
data/attachments/ Optional attachment storage

Docker Compose

This is the basic Compose layout I used.

services:
  ntfy:
    image: binwiederhier/ntfy:latest
    container_name: ntfy
    command:
      - serve
    restart: unless-stopped
    ports:
      - "8085:80"
    environment:
      - TZ=America/Chicago
      - NTFY_CONFIG_FILE=/etc/ntfy/server.yml
    volumes:
      - /mnt/user/appdata/ntfy/server.yml:/etc/ntfy/server.yml:ro
      - /mnt/user/appdata/ntfy/data:/var/lib/ntfy
Setting Why I used it
8085:80 Keeps ntfy off Unraid's main web ports
/etc/ntfy/server.yml Lets me manage config from appdata
/var/lib/ntfy Keeps user data and cache persistent
restart: unless-stopped Starts ntfy again after reboots
TZ=America/Chicago Keeps logs and timestamps aligned with my timezone

Port choice

The host port can be changed. I used a non-standard local port so it would not conflict with other services on Unraid.

Sample server.yml

This is a safe example config. Replace the placeholder domain with your own public ntfy hostname if you expose it later.

# Public URL for your ntfy instance.
# Use your real HTTPS URL only after the reverse proxy or tunnel is working.
base-url: "https://ntfy.example.com"

# Listen inside the container.
# Docker maps this to the host port from docker-compose.yml.
listen-http: ":80"

# Persistent storage.
cache-file: "/var/lib/ntfy/cache.db"
auth-file: "/var/lib/ntfy/user.db"
attachment-cache-dir: "/var/lib/ntfy/attachments"

# Keep anonymous users from reading or writing by default.
# Users, tokens, and access rules should be managed intentionally.
auth-default-access: "deny-all"

# Recommended when ntfy is behind a reverse proxy or Cloudflare Tunnel.
behind-proxy: true

# Optional attachment limits.
# Adjust or remove these based on your needs.
attachment-total-size-limit: "1G"
attachment-file-size-limit: "15M"
attachment-expiry-duration: "3h"

# Optional message cache duration.
cache-duration: "12h"

# iOS push support for self-hosted ntfy.
# This lets the iOS app use ntfy.sh as the upstream push relay.
upstream-base-url: "https://ntfy.sh"

Local-only testing

If you are only testing locally, base-url can be left out temporarily. Once you expose ntfy through HTTPS, set base-url to the final public URL.

Starting ntfy

From the ntfy appdata folder:

docker compose up -d

Check the logs:

docker compose logs -f ntfy

The local web UI should be available at:

http://SERVER-IP:8085

Authentication

For my setup, I wanted ntfy to be private by default. The important setting is:

auth-default-access: "deny-all"

That means anonymous users cannot read or write unless access is explicitly allowed.

A basic admin user can be created from inside the container:

docker compose exec ntfy ntfy user add --role=admin USERNAME

Tokens can be created for apps, scripts, and services:

docker compose exec ntfy ntfy token add USERNAME

List tokens:

docker compose exec ntfy ntfy token list USERNAME

Tokens are secrets

Bearer tokens should be treated like passwords. Do not commit them to GitHub, paste them into public docs, or show them in screenshots.

Sending a test notification

After creating a user or token, send a test message.

Using a bearer token:

curl \
  -H "Authorization: Bearer tk_REPLACE_WITH_TOKEN" \
  -H "Title: ntfy Test" \
  -H "Priority: default" \
  -d "Hello from my homelab." \
  https://ntfy.example.com/example-topic

Using local access while testing:

curl \
  -H "Authorization: Bearer tk_REPLACE_WITH_TOKEN" \
  -H "Title: Local ntfy Test" \
  -d "This was sent from inside the LAN." \
  http://SERVER-IP:8085/example-topic
Header Purpose
Authorization Authenticates the request
Title Notification title
Priority Controls alert importance
Tags Optional emoji/icon tags
Message body Main notification text

Mobile app setup

In the ntfy mobile app:

  1. Add your self-hosted ntfy server URL.
  2. Log in with your ntfy user.
  3. Subscribe to the topics you want to receive.
  4. Send a test notification.
  5. Confirm the notification arrives on your phone.

Topic names

Do not use obvious public topic names. Even with authentication enabled, I prefer using boring, non-public examples in screenshots and documentation.

Cloudflare Tunnel notes

I exposed ntfy using a Cloudflare Tunnel instead of opening ports on my router.

The public hostname points to the local ntfy container:

Public hostname Local service
https://ntfy.example.com http://SERVER-IP:8085

After the tunnel is working, the ntfy config should include:

base-url: "https://ntfy.example.com"
behind-proxy: true

Then restart ntfy:

docker compose restart ntfy

Why Cloudflare Tunnel

I already use Cloudflare Tunnels for other homelab services, so this kept the setup consistent and avoided router port forwarding.

Screenshots

If documenting Cloudflare Tunnel, blur tunnel IDs, account details, connector tokens, origin service URLs, and any private hostnames you do not want public.

Example use cases

Source Example notification
Backup script Backup completed or failed
Monitoring Service down or recovered
Media requests New request submitted
Docker maintenance Container update finished
Security alerts Unexpected login or exposed service warning

Example backup notification:

curl \
  -H "Authorization: Bearer tk_REPLACE_WITH_TOKEN" \
  -H "Title: Backup Complete" \
  -H "Tags: white_check_mark" \
  -d "The scheduled backup finished successfully." \
  https://ntfy.example.com/backups

Example alert notification:

curl \
  -H "Authorization: Bearer tk_REPLACE_WITH_TOKEN" \
  -H "Title: Service Alert" \
  -H "Priority: high" \
  -H "Tags: warning" \
  -d "A monitored service appears to be down." \
  https://ntfy.example.com/alerts

What I would not publish

Before pushing this to a public repo, I would check for the following:

Item Safe to publish?
Example Compose file Yes, if sanitized
Example server.yml Yes, if sanitized
Placeholder image paths Yes
Real bearer tokens No
Real passwords No
Real private topic names No
Cloudflare tunnel token No
Screenshots with secrets No
Internal-only notes No

GitHub check

Before committing, search the repo for tk_, password, token, secret, real domains, and private topic names.

Final thoughts

This ended up being a small but useful homelab project. ntfy gives me a flexible notification endpoint that works with scripts, webhooks, apps, and monitoring tools without adding a lot of complexity.

The biggest benefits are that it is easy to test locally, easy to integrate, and simple enough that I can keep expanding it as more services in my homelab need alerts.