备份脚本跑完、证书续期失败、磁盘快满了,这些消息如果只留在服务器日志里,往往要等下次登录才看见。ntfy 可以接收一个 HTTP 请求,把消息送到订阅了对应主题的手机或浏览器。发送端不必安装专门的客户端,能发 HTTP 请求就可以接入。
部署前先把权限配好。默认启动的 ntfy 允许任何人读写主题,换一个难猜的主题名,并不等于建立了私人通知服务。本文把它配置成默认拒绝访问,再分别给发送脚本和接收账号授权,同时说明 Android、iPhone 和浏览器的接收条件。
这套服务负责传消息,不负责判断服务器是否正常
ntfy 的主题可以理解成一个消息地址。备份脚本向 ops-backup 发消息,手机订阅同一个主题,就能收到它;站点检查可以使用另一个主题,避免所有事情挤在一处。
它并不会自动知道备份是否成功。判断依据来自调用它的脚本、监控工具或应用:程序在哪里判断失败,通知也要放在那个分支。如果脚本从来不运行,ntfy 自然没有消息可送。因此,“备份成功通知”只能说明发送端走到了对应位置,不能证明备份内容能够恢复;对于重要任务,还需要检查未按时执行的情况。
已有 Gotify 或监控告警渠道时,先看是否确实需要多个接收平台和 HTTP 主题接口。没有必要只因为看到一个新工具,就把已有通知系统全部迁走。下面按私人通知服务的用途配置账号,不开放公共主题。
准备一台 VPS、一个域名和 Docker Compose
以下配置使用 ntfy 2.28.0,这是截至 2026 年 10 月 3 日查到的最新发布版本。服务器先安装 Docker 与 Compose 插件,并准备一个自己的子域名,例如 notify.example.com。文中的这个地址是占位示例,配置和客户端里都要替换成实际域名。
反向代理使用安装在宿主机上的 Caddy,公网开放它需要的 HTTP/HTTPS 入口。域名解析到服务器,确认没有失效的 IPv6 解析干扰访问;如果已有网站占用端口,就把新域名加入现有 Caddy 配置,不要另起一个争用 80、443 的实例。
这套示例不接外部 PostgreSQL,使用本地 SQLite 保存消息缓存和账号权限。发文字通知也不需要对象存储。消息量、缓存保留时间和附件会影响磁盘占用,不能只凭“通知工具”几个字判断资源始终很少。
如果需要独立机器,可以查看雨云服务器方案,优惠码为 KuZhuJi。选择时确认手机所在网络能访问该地区服务器,套餐有合适的公网入口,域名使用条件也符合自己的情况。服务端跑起来与手机能及时收到,是两件需要分别检查的事情。
把数据与配置留下来
以普通 SSH 用户的家目录作为项目位置:
mkdir -p ~/ntfy/config ~/ntfy/data
cd ~/ntfy
chmod 700 config data
创建 config/server.yml:
base-url: "https://notify.example.com"
listen-http: ":80"
behind-proxy: true
cache-file: "/var/lib/ntfy/cache.db"
cache-duration: "12h"
auth-file: "/var/lib/ntfy/auth.db"
auth-default-access: "deny-all"
enable-login: true
enable-signup: false
再创建 compose.yaml:
services:
ntfy:
image: binwiederhier/ntfy:v2.28.0
container_name: ntfy
command: serve
restart: unless-stopped
ports:
- "127.0.0.1:8082:80"
volumes:
- ./config:/etc/ntfy:ro
- ./data:/var/lib/ntfy
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
auth-file 保存账号、授权和访问令牌;cache-file 保存消息缓存。两者放进同一个持久化目录,但作用不同。把容器删掉再创建时,data/ 中的内容仍在;使用其他路径或删除这个目录,则可能得到一个没有账号和旧消息的新实例。
cache-duration: "12h" 表示消息缓存期限,不是永久聊天记录。离线超过缓存窗口后,不要期待所有旧通知仍能补收。需要长期追溯的备份或监控记录,应保存在原来的任务日志或审计记录里。
这里使用镜像默认运行身份,没有添加任意 user:。如果你自行指定容器用户,必须同步检查 data/ 的写权限,否则认证库和缓存可能建不起来。配置目录是只读挂载,CLI 管理账号仍可写入 data/ 中的认证库。
先检查 Compose 格式并启动:
docker compose config
docker compose up -d
docker compose logs --tail=100 ntfy
格式检查能发现 YAML 或变量展开问题;域名、TLS、账号权限和手机接收要分别检查。日志有错误时先处理启动问题,不要直接把“容器存在”当作部署完成。
用 Caddy 提供 HTTPS 入口
在宿主机 Caddy 的配置中添加:
notify.example.com {
reverse_proxy 127.0.0.1:8082
}
示例中的反向代理地址是宿主机回环端口。若 Caddy 本身运行在另一个容器里,127.0.0.1 指向的是那个 Caddy 容器,需要改用共享 Docker 网络和 ntfy 服务名,不能照抄这个地址。
使用系统服务安装的 Caddy,可以先验证再重载:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
Caddy 根据域名提供 HTTPS,ntfy 的 base-url 也必须使用对外的 HTTPS 地址。behind-proxy: true 告诉 ntfy 从代理头中识别客户端地址;只有可信代理应该能访问后端,所以示例只把 8082 映射到宿主机回环地址,不额外对公网开放它。
不要再给整个域名套一个与 ntfy 无关的网页登录拦截,除非你已确认原生客户端和脚本如何通过。消息 API、长连接订阅与浏览器页面共用这个服务,网页看似正常,不代表手机或发送脚本也能通过另一套认证。
发送账号只写,接收账号只读
在项目目录执行下面的命令,交互输入两套不同密码:
docker compose exec ntfy ntfy user add backupbot
docker compose exec ntfy ntfy user add reader
它们都是普通用户,默认没有任何主题访问权限。给 backupbot 写入 ops-backup 的权限,给 reader 读取同一主题的权限:
docker compose exec ntfy ntfy access backupbot ops-backup write-only
docker compose exec ntfy ntfy access reader ops-backup read-only
docker compose exec ntfy ntfy access
这组权限有明确的用途:发送脚本可以写通知,但不能读取这个主题的历史内容;手机上的接收账号可以看消息,但不能冒充备份脚本发布“备份成功”。其他主题继续由默认拒绝策略保护。
不要为了省两条授权命令就把脚本账号设成管理员。ntfy 的 admin 角色可以读写全部主题,细粒度 ACL 不会把管理员限制在 ops-backup。需要管理全站时再单独创建管理员账号,不要把它的凭据分发给每个任务。
如果以后使用 ops-* 这样的通配主题,要先考虑它是否涵盖了原本不打算开放的主题。主题名不是保密凭据,也不是用户名;知道主题名的人,仍应经过账号授权才能读写。
给脚本发令牌,别共用手机密码
给发送账号建立一枚有效期为 30 天、带用途标签的令牌:
docker compose exec ntfy ntfy token add --expires=30d --label=backup-job backupbot
命令会输出令牌,应当按密码保存。这里用有效期说明轮换方式,并不要求所有任务都固定为 30 天;选多长时间,要结合能否及时更换脚本中的凭据。到期后发送会失败,需要提前安排轮换。
令牌继承对应用户的权限。标签 backup-job 只是说明用途,不能自动把令牌限制到某个主题;只写 ops-backup 的边界来自前面给 backupbot 设置的 ACL。若多个任务要求不同权限,使用不同用户,不要仅创建多个标签就以为已经隔离。
在 Bash 中发送第一条文字消息,可以临时读取令牌,避免直接把真实值写进示例命令和 shell 历史:
read -r -s -p "ntfy token: " NTFY_TOKEN
printf '\n'
curl --fail --silent --show-error \
-H "Authorization: Bearer ${NTFY_TOKEN}" \
-H "Title: Backup job" \
--data-binary 'Backup command finished. Check the backup log.' \
https://notify.example.com/ops-backup
unset NTFY_TOKEN
这段示例没有上传数据库或日志附件,只发一条状态消息。不要把完整访问令牌、数据库连接串、客户数据或整段错误日志作为通知内容。即使主题有权限限制,消息也可能显示在手机锁屏上。
长期任务应把凭据放在仅运行用户可读的文件或凭据管理机制中,不提交到 Git。用 curl -H 传值仍可能让本机有权限查看进程的用户观察到参数;有多用户隔离要求的机器,需要再选择符合自己凭据管理规则的传递方式。
在手机上确认自己订阅的是这台服务器
安装 ntfy 客户端后,添加主题时选择自己的服务器地址 https://notify.example.com,主题填写 ops-backup。接收账号使用 reader 和它的密码。不要仍使用默认的公共服务器地址,否则你订阅的是另一台服务器上的同名主题。

图中是官方文档的 Android 历史界面示例(截图日期为 2021 年),展示主题内的通知内容;实际界面随客户端版本变化。
Android 客户端文档说明了即时接收方式:自建服务器不使用公共 ntfy.sh 的 Firebase 通道,需要客户端维持对应的订阅连接。F-Droid 版不包含 Firebase,默认使用即时接收。还要检查系统通知权限、后台运行和省电设置,不能只在打开 App 时看见消息就认定锁屏后也正常。
iPhone 的限制不同。服务端配置文档说明,自建服务器要实现即时通知,需要向上游发送唤醒请求,再由客户端回到自己的服务器拉取原消息。需要这一功能时,可以在 server.yml 追加:
upstream-base-url: "https://ntfy.sh"
然后重启服务:
docker compose restart ntfy
这意味着 iOS 即时通知链路仍依赖上游及系统推送服务,不能称为所有环节都独立。上游收到的是用于唤醒的轮询请求,不是整条原消息正文;对这一依赖有要求时,先阅读官方说明再决定是否开启。不配置上游,消息可能延迟,不能承诺与前台收取相同的到达速度。
浏览器的 Web Push 又是另一项配置,需要推送密钥、持久化订阅信息等条件。本篇没有开启它。能打开网页并浏览通知,不表示关掉网页后仍会收到系统推送。
检查权限时,要检查被拒绝的情况
第一条消息正常到达后,还应确认匿名请求无法写入私人主题:
curl --silent --show-error \
-o /dev/null -w '%{http_code}\n' \
--data-binary 'anonymous permission check' \
https://notify.example.com/ops-backup
这里应返回拒绝访问,而不是成功发布。如果匿名请求也能写入,检查生效的配置文件、默认访问策略以及是否另有匿名授权;不要用改主题名掩盖权限配置问题。
接着分别用发送账号尝试读取、用接收账号尝试写入,确认也被拒绝。检查时使用不包含业务数据的消息;完成后再把真实任务接过来。命令返回成功只能确认服务端接受了请求,手机是否显示、锁屏后是否收到、离线一段时间能否补收,都要在接收端看。
多个机器共用通知服务时,最好每个任务使用独立发送身份。某台机器停用后撤销对应令牌,不必更换所有手机的密码。可以查看与删除令牌:
docker compose exec ntfy ntfy token list backupbot
docker compose exec ntfy ntfy token remove backupbot TOKEN_TO_REMOVE
TOKEN_TO_REMOVE 是占位符,替换为要撤销的实际令牌。不要把完整令牌清单贴到公开排障帖子中。
更新与备份,把权限库一起保存
升级前记录当前镜像标签,并保存 compose.yaml、config/ 和 data/。这套配置使用 SQLite,停服务后归档整个项目目录,可以避免复制过程中数据库仍在写入,也一并保存可能存在的 SQLite 辅助文件。
在 ~/ntfy 中操作,备份文件放在项目目录之外:
docker compose stop ntfy
umask 077
sudo tar -czf - -C "$HOME" ntfy \
> "$HOME/ntfy-backup-$(date +%Y%m%d-%H%M%S).tar.gz"
docker compose start ntfy
停服务期间无法正常接收新请求,发送端应有自己的失败处理。示例让 tar 有权限读取容器创建的文件,再由当前用户写出归档。这个备份包含认证库和历史消息,需要按敏感数据保护,并保留一份不依赖当前 VPS 的副本。备份打包失败时,先检查错误并让服务恢复,再修正归档问题;不能因为前一个命令失败就忘了服务仍然停着。
恢复时将归档解到独立目录,先使用原来的镜像版本与配置检查账号、授权和数据是否存在。不要直接覆盖运行中的数据库。恢复后的发布、订阅和匿名拒绝都需要重新确认,然后再切换域名或任务入口。
以后更新镜像时,应明确修改版本标签并阅读相应发布记录。数据库格式变化时,仅把镜像标签改回去未必能回滚,旧版镜像应配合更新前的数据副本。通知丢失也不要只盯着服务端:任务有没有执行、请求有没有被接受、客户端有没有保持连接,这三处都有各自的记录可查。











