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:
- Add your self-hosted ntfy server URL.
- Log in with your ntfy user.
- Subscribe to the topics you want to receive.
- Send a test notification.
- 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.