综合 自建 ntfy 推送通道:Docker 部署、Token 认证与 Nginx 长连接反代

2026-10-23 20:00:57

自建 ntfy 推送通道:Docker 部署、Token 认证与 Nginx 长连接反代

项目信息

  • GitHub:https://github.com/binwiederhier/ntfy
  • 文档:https://docs.ntfy.sh

为什么个人站需要一条自己的推送通道

半夜磁盘写满、PHP-FPM 进程池打满、网站返回 502,你要等到第二天用户投诉才知道。Prometheus、Zabbix、Uptime Kuma 都装了,问题在于最后一段:告警怎么落到手机上。

常见的土办法有三种:告警发邮件,手机邮箱 App 推送经常延迟,或者被系统省电策略杀掉;发到微信、钉钉、飞书群,公司场景可以,个人站挂个机器人有点小题大做,而且第三方接口说变就变;买 Server 酱一类托管服务,免费额度有限,敏感信息还要过别人的服务器。

ntfy(读作 notify)是自托管圈子里常见的一个答案。Go 写的单文件二进制,服务端客户端全开源,跑在自己的服务器上,通过长连接把消息推到手机。

ntfy 的定位

推送架构是发布 / 订阅:脚本向某个 topic 发一条 HTTP POST,订阅了该 topic 的手机 App 立刻收到通知。消息内容经过你自己的服务器。

  • 对比邮件:邮件是拉取式,客户端受系统省电策略影响大,延迟从几秒到几十分钟不等。ntfy 走常驻长连接加系统级推送,正常几秒内到达。
  • 对比微信 / 钉钉机器人:那是别人的平台,接口会变、会限流、会封机器人,告警内容也全在对方服务器上。
  • 对比托管推送服务:自建没有月额度限制,也不担心服务商跑路。

ntfy 同时提供网页端订阅,任何浏览器打开一个 URL 就能收消息,临时调试或者手边没有手机时能用。

服务端部署:Docker Compose

mkdir -p /opt/ntfy/{cache,etc}
cd /opt/ntfy

docker-compose.yml:

services:
  ntfy:
    image: binwiederhier/ntfy
    container_name: ntfy
    command:
      - serve
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - ./cache:/var/cache/ntfy
      - ./etc:/etc/ntfy
    ports:
      - "127.0.0.1:2586:80"
    restart: unless-stopped

端口只绑 127.0.0.1,不要写成 2586:80(那等于绑到 0.0.0.0,全网可访问)。ntfy 默认没有认证,任何人只要知道你的域名和 topic 名就能发消息,也能订阅你的 topic 读消息——topic 名本身不是密钥,是可以猜的。先绑回环,后面用 Nginx 反代再加认证。

docker compose up -d
docker compose logs -f ntfy

看到 listening on [::]:80 之类的日志就起来了。本机验证:

curl -d "ntfy 服务已启动" http://127.0.0.1:2586/mytopic

返回一串 JSON 即发布成功。

配置文件

命令行参数适合试跑,长期运行要写配置文件。ntfy 读取容器内的 /etc/ntfy/server.yml,对应宿主机挂载的 ./etc/server.yml。

base-url: "https://ntfy.example.com"
listen-http: ":80"
cache-file: "/var/cache/ntfy/cache.db"
cache-duration: "48h"
behind-proxy: true
auth-file: "/var/cache/ntfy/user.db"
auth-default-access: "deny-all"
  • base-url:必须填最终对外访问的 HTTPS 地址,否则 App 收到的点击跳转链接会指向内网 IP,点开打不开。新手最常见的坑。
  • cache-file / cache-duration:消息落地到磁盘,手机离线一会儿再上线也能收到积压消息,这里保留 48 小时。
  • behind-proxy: true:告诉 ntfy 它在 Nginx 后面,需要信任 X-Forwarded-For 来识别真实客户端 IP。
  • auth-default-access: deny-all:默认拒绝匿名访问,只有授权用户才能收发。

开启认证

改完配置先重启一次让文件生效:

docker compose restart ntfy

# 创建管理员
docker exec -it ntfy ntfy user add --role=admin admin

# 创建普通用户(只给读写权限)
docker exec -it ntfy ntfy user add sender
docker exec -it ntfy ntfy access sender "*" read-write

user add 会提示输入密码。发告警用 sender 这个账号,权限只给 read-write,不给 admin。

配好认证后,发布消息必须带凭据。Basic Auth:

curl -u sender:你的密码 -d "测试告警" https://ntfy.example.com/mytopic

更推荐访问令牌(access token),适合脚本和 systemd 服务,避免密码到处写:

docker exec -it ntfy ntfy token add sender
# 输出形如 tk_xxxxxxxxxxxx,记录下来
curl -H "Authorization: Bearer tk_xxxxxxxxxxxx" -d "测试告警" https://ntfy.example.com/mytopic

token 本身不等于全权限——它绑定到某个用户,能读能写哪些 topic 由该用户在 auth-file 里的 ACL 决定,所以给 sender 的 read-write 范围要收窄。

Nginx 反向代理

ntfy 依赖长连接,反代配置写错会出现「网页能打开但手机收不到消息」。

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;

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

        # 长连接/WebSocket 必须的头部
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # 关闭缓冲,否则消息会被"攒着"延迟下发
        proxy_buffering off;

        # 长连接不能按默认 60s 超时断开
        proxy_read_timeout 1d;
        proxy_send_timeout 1d;

        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;
    }
}

三处关键点:proxy_buffering off 必须关,否则 Nginx 会把流式响应攒在缓冲区,消息到 App 有明显延迟;proxy_read_timeout 要拉长(1d),App 的订阅连接是长期挂着的,默认 60 秒会被掐断导致反复重连;Upgrade 和 Connection 头是必须的。

证书:

certbot --nginx -d ntfy.example.com

手机端接入

应用商店装官方 App「ntfy」(Android 在 Play 商店或 F-Droid,iOS 在 App Store)。首次打开:

  1. 设置里把 Default server 改成 https://ntfy.example.com,否则默认连官方 ntfy.sh。
  2. 订阅你的 topic,比如 mytopic。
  3. 服务端开了认证时,App 里填用户名密码,或粘贴 token。

iOS 有个情况要提:ntfy 在 iOS 上依赖 Apple 推送服务(APNs)兜底长连接,但自建实例要启用 APNs 需要自行托管转发服务,配置比 Android 麻烦。个人用户的折中方案是 iOS 上同时开着 App 后台长连接(系统设置里允许 ntfy 后台刷新)。Android 相对简单,前台服务常驻即可。

ntfy 也支持用 URL 直接订阅并带参数,适合做成「一键订阅」链接:

ntfy://ntfy.example.com/mytopic

接进监控和脚本

场景一:磁盘写满告警。

#!/bin/bash
THRESHOLD=85
TOKEN="tk_xxxxxxxxxxxx"
USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$USAGE" -ge "$THRESHOLD" ]; then
  curl -s \
    -H "Authorization: Bearer $TOKEN" \
    -H "Title: 磁盘告警" \
    -H "Priority: high" \
    -H "Tags: warning" \
    -d "根分区已用 ${USAGE}%,请及时清理" \
    https://ntfy.example.com/mytopic
fi

ntfy 允许用 HTTP 头指定标题、优先级和标签。Priority: high 让手机以更醒目的方式提醒,Tags 显示图标。

场景二:systemd 服务失败自动通知。

[Unit]
Description=Notify on failure

[Service]
Type=oneshot
ExecStart=/usr/local/bin/notify-failure.sh "%i"

[Install]
WantedBy=multi-user.target

在被监控的服务里加 OnFailure=notify-failure@%n.service,服务崩溃时 systemd 触发这个 oneshot 单元推消息。

场景三:接进 Uptime Kuma。 Uptime Kuma 内置 ntfy 通知方式,在「设置 → 通知」里选 ntfy,填服务器地址、topic 和 token,网站一宕机就推送。

场景四:部署完成通知。 在 CI/CD 或部署脚本最后加一行 curl,成功失败都推一条。

进阶:用心跳防「安静地坏掉」

告警系统最怕的不是误报,而是推送通道本身挂了、或者脚本被注释掉了,你却以为一切正常。

做法是准备两个 topic:alerts 收正常告警,heartbeat 由正常运行的机器定时发「我活着」。如果 heartbeat 超过预期时间没消息,说明某台机器或某个服务挂了。Healthchecks、Uptime Kuma 的 Push 类型监控都内置了这种反向确认逻辑。

ntfy 支持消息的 Delay 头做延时投递,可以配合实现「N 分钟内没有新事件就提醒」这类逻辑。

六个最容易踩的坑

  1. 端口绑到 0.0.0.0 且没开认证:任何人扫到 2586 就能发消息、订阅偷听。永远先绑 127.0.0.1,对外只走 Nginx,配好 deny-all 和用户认证。
  2. base-url 填了内网地址:手机端点击通知跳转到内网 IP,打不开。必须填最终 HTTPS 域名。
  3. 反代漏了 proxy_buffering off:消息延迟严重,甚至攒一批一起发。这是长连接类服务反代的通病。
  4. 没配 proxy_read_timeout:默认 60 秒断开,手机端反复重连,耗电且丢消息。
  5. token 和密码写死在脚本里并提交到 Git:仓库一公开等于把推送通道送人。用环境变量或 chmod 600 的凭据文件。
  6. 只监控不验证:装完做一次断网演练——手动停掉一个服务,确认告警真的到了手机上。没验证过的告警通道等于没有。

小结

ntfy 用一个极简的 HTTP 模型把「服务器事件」和「人」之间的最后一段打通了:单二进制、无外部依赖、支持 Docker,也支持无 App 的网页订阅。

部署顺序:Docker 起来 → 配置文件定住 base-url 和缓存 → 开认证建 token → Nginx 反代关 buffering、拉长超时 → 手机 App 改默认服务器 → 用 curl 接进磁盘告警和服务失败通知 → 最后做一次断网演练。

复制全文 生成海报 ntfy 运维 Docker Nginx 告警

推荐文章

程序员茄子在线接单