ntfy 自建消息推送服务实战:告警秒到手机,Docker 部署、令牌认证与 Nginx 长连接反代全流程

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

做站久了,你一定遇到过这种场面:半夜服务器磁盘写满、PHP-FPM 进程池打满、网站返回 502,而你正在睡觉,直到第二天早上有用户投诉才知道。监控工具(Prometheus、Zabbix、Uptime Kuma)你其实都装了,问题出在最后一百米——告警怎么送到你手机上。

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

ntfy(读作 notify)是这几年在自托管圈子里很受欢迎的一个答案。它用 Go 写成,就是一个单文件二进制,服务端和客户端全开源,跑在你自己的服务器上,通过一个长连接把消息推到手机上。本文就从零把 ntfy 部署起来,接到你的监控和脚本里,并把它安全地放在公网后面。

ntfy 到底解决了什么问题:它和那三个土办法的区别

先讲清楚定位,避免你装完发现不合适。

推送架构:ntfy 采用「发布/订阅」模型。你的服务器脚本向某个 topic 发一条 HTTP POST,订阅了这个 topic 的手机 App(通过长连接或 FCM 兜底)立刻收到通知。整个过程你自己掌控,消息内容经过你的服务器。

对比邮件:邮件是拉取式(或半推半拉),客户端受系统省电策略影响大,延迟可以从几秒到几十分钟不等。ntfy 走的是常驻长连接 + 系统级推送,正常几秒内到达。

对比微信/钉钉机器人:那些是别人的平台,接口会变、会限流、会封机器人,而且你的告警内容全在对方服务器上。个人站自建没有这些顾虑。

对比托管推送服务:自建最大的好处是没有月额度限制,也不担心服务商跑路或者把你的数据拿去做什么。

ntfy 的另一个杀手级特点是「无 App 也能用」——它同时提供网页端订阅,任何浏览器打开一个 URL 就能收消息,临时调试或者在没有手机的场景下非常方便。

服务端部署:Docker Compose 三分钟起来

最省心的方式是 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 偷看消息。先绑回环,后面用 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 控制保留多久。
  • behind-proxy: true 告诉 ntfy 它在 Nginx 后面,需要信任 X-Forwarded-For 头来识别真实客户端 IP。
  • auth-default-access: deny-all 是安全底线——默认拒绝匿名访问,只有授权的用户才能收发。

开启认证:给推送加一把锁

有了 auth-file 配置后,用容器内的命令创建用户。注意先重启一次让配置文件生效:

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

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 秒会被 Nginx 掐断导致反复重连;Upgrade 和 Connection 头是必须的。

证书用 certbot 一键签:

certbot --nginx -d ntfy.example.com

手机端接入:Android 和 iOS 各有讲究

去应用商店装官方 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 有个特殊情况要提醒:iOS 上 ntfy 依赖 Apple 的推送服务(APNs)作为长连接的兜底,但自建实例要启用 APNs 需要自行托管转发服务,配置比 Android 麻烦,很多个人用户的折中方案是在 iOS 上同时开着 App 后台长连接(在系统设置里允许 ntfy 后台刷新)。Android 则相对简单,前台服务常驻即可。

另外 ntfy 支持用 URL 直接订阅并指定参数,非常适合做成一个「一键订阅」链接:

ntfy://ntfy.example.com/mytopic

接进你的监控和脚本:几个真实场景

有了推送通道,接下来的问题是怎么让各种系统都用上它。

场景一:磁盘写满告警。写一个 cron 脚本:

#!/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:

[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,部署成功或失败都推一条,比翻日志省事得多。

进阶:给自己的脚本用一个「心跳」防沉默

告警系统最怕的不是误报,而是「安静地坏掉」——推送服务本身挂了,或者脚本被注释掉了,你却以为一切正常。解决方法是所谓的心跳(heartbeat)机制。

做法是准备两个 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:消息延迟严重,甚至会「攒一批一起发」。这是长连接类服务反代的通病,不是 ntfy 独有。
  4. 没配 proxy_read_timeout:默认 60 秒断开,手机端反复重连,耗电且丢消息。
  5. token 和密码写死在脚本里并提交到 Git:一旦仓库公开就等于把推送通道送人。用环境变量或一个 chmod 600 的凭据文件。
  6. 只监控不验证:装完 ntfy 一定要做一次「断网演练」——手动把一个服务停掉,确认告警真的到了手机上。没验证过的告警通道等于没有。

小结

ntfy 的价值不在于技术多复杂,而在于它用一个极简的 HTTP 模型,把「服务器事件」和「人」之间的最后一米打通了。它单二进制、无外部依赖、支持 Docker、也支持无 App 的网页订阅,非常适合个人站长这种「自己就是运维」的场景。

部署要点回顾:Docker 起来 → 配置文件定住 base-url 和缓存 → 开认证建 token → Nginx 反代记得关 buffering、拉长超时 → 手机装 App 改默认服务器 → 用 curl 接进磁盘告警和服务失败通知 → 最后做一次断网演练确认真的能收到。做到这几步,你半夜就再也不用靠用户投诉来发现服务器出问题了。

Last modification:October 16th, 2026 at 12:25 pm

Leave a Comment