自建 ntfy:Docker 服务端部署与手机、桌面客户端接入
ntfy 可以把一次 HTTP 请求变成手机或桌面上的通知。脚本完成、服务器告警、博客催更,都可以通过同一套服务发布消息,客户端按主题订阅。
自建之后,最容易出错的不是启动容器,而是把几组配置混在一起:手机应该填写公网服务地址,不能填写 Docker 服务名;订阅账号需要读取权限,不能直接使用告警程序的只写 Token;消息已经进入 ntfy,也不等于手机后台已经弹出了通知。
本文给出一套独立的安装与接入流程。服务端以本站实际接入时使用的 v2.11.0 为版本基线,不把它当作最新版本推荐;新部署应检查官方发布说明,选择并固定经过验证的版本。客户端说明参考官方文档,菜单名称可能随版本变化。域名、主题和路径均为示例,不包含实际部署凭据。
先看清服务端、发布端和订阅端
Alertmanager → 告警转发脚本 ─┐ ├─ HTTP 发布 → ntfy 服务端 → Android / iOS / Web博客催更接口 ────────────────┘ ↑ 账号与主题权限ntfy 服务端负责接收、缓存和分发消息。告警脚本、博客接口是发布端,手机应用和网页是订阅端。发布端不一定需要读取权限,订阅端也不一定需要发布权限。
本文统一使用以下示例,部署时需要整体替换,不能只改其中一处:
| 项目 | 示例值 | 用途 |
|---|---|---|
| 公网服务地址 | https://ntfy.example.com | 手机、桌面和跨主机发布程序访问 |
| 主题 | blog-updates | 发布端与订阅端必须完全一致 |
| 发布账号 | blog-reminder | 对目标主题只写 |
| 订阅账号 | notify-reader | 对目标主题只读 |
| 内网发布地址 | http://ntfy/blog-updates | 仅供处于同一 Docker 网络的程序使用 |
一条订阅由服务地址和主题共同确定。 https://ntfy.sh/blog-updates 与 https://ntfy.example.com/blog-updates 是两条不同的通知流。
ntfy 不需要先执行「创建主题」命令。这里真正需要提前配置的是账号及其主题权限;在默认拒绝访问的实例上,知道主题名并不代表可以读写。
用 Docker 部署服务端
准备目录和配置文件
以下示例假设 Linux 主机已安装 Docker Engine 和 Compose 插件,并准备由同一宿主机上的 Nginx 终结 TLS。已有 ntfy 实例应先备份配置和数据,不要直接覆盖。
sudo mkdir -p /srv/ntfy/data/cache /srv/ntfy/data/lib在 /srv/ntfy/server.yml 中写入:
base-url: "https://ntfy.example.com"listen-http: ":80"behind-proxy: true
cache-file: "/var/cache/ntfy/cache.db"cache-duration: "12h"
auth-file: "/var/lib/ntfy/user.db"auth-default-access: "deny-all"enable-login: true
# 使用官方 iOS App 的后台即时通知时需要,相关隐私边界见后文。upstream-base-url: "https://ntfy.sh"这份配置没有开放附件上传,先把纯文本通知跑通。需要附件时再配置单独的附件目录、大小和保留时限,不要把其他文件放进 ntfy 管理的附件目录。
三个关键点:
base-url填客户端实际访问的公网 HTTPS 地址,不填容器名或带主题的地址。auth-default-access: deny-all配合auth-file,默认拒绝未授权读写;enable-login只是启用 Web 登录入口,不能单独代替访问控制。cache-file让缓存落盘。只设缓存时长、不设持久化文件,不能保证消息缓存跨重启保留。
cache-duration: 12h 是消息在服务端的保留时间,与博客按 IP 12 小时去重无关。修改这里的缓存时长,不会改变催更服务的计数窗口。
配置 Compose 并启动
在 /srv/ntfy/compose.yml 中写入:
name: ntfy
services: ntfy: image: binwiederhier/ntfy:v2.11.0 command: serve restart: unless-stopped ports: - "127.0.0.1:8080:80" volumes: - ./server.yml:/etc/ntfy/server.yml:ro - ./data/cache:/var/cache/ntfy - ./data/lib:/var/lib/ntfyserver.yml 必须事先存在且是文件,否则绑定挂载可能产生错误的目录。数据目录需要允许容器实际运行用户写入;如果自行指定非 root UID,应同步处理宿主目录权限,不要通过 chmod 777 绕过问题。
启动并查看状态:
docker compose -f /srv/ntfy/compose.yml config --quietdocker compose -f /srv/ntfy/compose.yml up -ddocker compose -f /srv/ntfy/compose.yml psdocker compose -f /srv/ntfy/compose.yml logs --tail 50 ntfy然后在宿主机检查健康接口:
curl --fail-with-body -sS http://127.0.0.1:8080/v1/health监听在回环地址,可以避免绕过反向代理从公网直接访问明文端口。如果 Nginx 或 Traefik 也运行在容器里,不能让它连接自己的 127.0.0.1:8080。 应改为共享受控 Docker 网络,通过 http://ntfy:80 转发,并根据实际拓扑决定是否还需要宿主端口映射。
本站现有环境通过容器网络和公网网关转发,原理相同,但不是本文的宿主机 Nginx 示例拓扑。
配置公网 HTTPS 入口
前置条件是域名已经解析到入口地址,证书覆盖 ntfy.example.com,并提供设备信任的完整证书链。以下示例假设证书已存在;证书签发和续期应交给现有自动化流程维护。
Web 客户端可能使用 WebSocket,订阅也会使用长连接。代理必须保留这些行为,不能只验证首页能返回 HTML。
# map 放在 http 上下文中,不要放进 server。map $http_upgrade $ntfy_connection { default upgrade; '' close;}
server { listen 80; server_name ntfy.example.com; return 301 https://$host$request_uri;}
server { listen 443 ssl; 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:8080; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $ntfy_connection; proxy_buffering off; proxy_read_timeout 1h; }}这是公网直接进入此 Nginx 的单跳示例。若前面还有 CDN 或其他网关,应先配置可信代理来源,不能照抄后就宣称拿到了真实客户端 IP。behind-proxy: true 依赖可信入口提供正确的转发头,不应同时允许任意客户端绕过入口直连后端。
通过语法检查后再重载:
sudo nginx -tsudo systemctl reload nginxcurl --fail-with-body -sS https://ntfy.example.com/v1/health不要为了原生客户端接入,在 ntfy API 前再强制跳转到一个需要浏览器交互的统一登录页面。机器程序和手机 App 需要得到 ntfy 的 API 响应,而不是登录页 HTML。确需网关认证时,必须验证客户端兼容性,同时保留 ntfy 自己的主题访问控制。
分开创建发布账号和订阅账号
以下命令在 ntfy 所在服务器执行。创建用户时交互输入随机密码,不把明文密码写在命令行参数里:
docker compose -f /srv/ntfy/compose.yml exec ntfy ntfy user add blog-reminderdocker compose -f /srv/ntfy/compose.yml exec ntfy ntfy user add notify-reader再授予最小权限:
docker compose -f /srv/ntfy/compose.yml exec ntfy \ ntfy access blog-reminder blog-updates write-only
docker compose -f /srv/ntfy/compose.yml exec ntfy \ ntfy access notify-reader blog-updates read-only
docker compose -f /srv/ntfy/compose.yml exec ntfy ntfy access预期结果是:发布账号只能写这个主题,订阅账号只能读这个主题,匿名用户仍无权访问。若已有账号或主题,应先查看现有 ACL,再增加所需权限,不要为了接一个客户端重置全局权限。
下一条命令会输出完整 Token。 在受控终端执行,将结果写入发布程序的受限配置文件,不要放进文章、截图或公共日志:
docker compose -f /srv/ntfy/compose.yml exec ntfy \ ntfy token add --label=blog-reminder blog-reminderToken 继承所属账号的权限,不是创建一个不同标签就自动变成不同权限。要隔离发布和订阅,应使用不同账号及 ACL,而不是把管理员 Token 分发给每台设备。
手机端优先使用 notify-reader 的用户名和密码。只有客户端明确提供 Token 或自定义认证入口时,才使用订阅账号对应的 Token;不要把只写的 blog-reminder Token 当作订阅凭据,也不要假设把 Bearer Token 直接填进密码框总能工作。
客户端连接参数
手机或桌面客户端需要的是这组配置:
| 客户端字段 | 填写内容 |
|---|---|
| 服务器、基础 URL 或自定义服务器 | https://ntfy.example.com |
| 主题、Topic | blog-updates |
| 用户名 | notify-reader |
| 密码 | 创建 notify-reader 时设置的密码 |
| 通知权限 | 允许,并检查主题是否静音 |
如果客户端只提供一个完整订阅 URL 字段,则填写 https://ntfy.example.com/blog-updates。如果服务地址和主题分成两个字段,就按表格分别填写,不要把主题重复拼接。
不应填写的值包括:http://ntfy、Docker 私网 IP、博客域名、/api/remind,以及服务端 iOS 配置中的 https://ntfy.sh。后者是唤醒上游,不是手机要改订阅的服务器。
账号也属于特定服务器。即使在 ntfy.sh 注册了同名用户,它也不能代替自建实例中的 notify-reader。
Android 安装与订阅
官方 Android 应用可从 Google Play、F-Droid 或 官方 APK 发布页 安装。不要使用来源不明的修改包。
- 安装 ntfy,允许系统通知权限。
- 添加订阅,选择其他服务器或自定义服务器,填写公网基础 URL 和主题。
- 在该服务器的认证提示或用户设置中添加
notify-reader及密码。账号必须关联到同一个服务器地址。 - 打开主题,先确认没有连接或授权错误,再让发布端发送一条测试消息。
- 熄屏并等待设备进入后台状态后再测试,检查是否仍能收到通知。
自建服务器不能直接套用 ntfy.sh 的 Firebase 投递行为。官方文档说明,Android 自建订阅使用即时投递机制;F-Droid 版本不包含 Firebase,所有订阅默认使用即时投递,因此不一定显示同名开关。
若前台正常、熄屏延迟,应检查系统电池优化、后台运行限制和厂商自启动设置,而不是先去开放匿名权限。保留前台订阅服务可能带来常驻通知,这是维持后台连接的一部分。
iPhone / iPad 安装与订阅
从 App Store 的官方 ntfy 页面 安装,允许通知,然后添加自建服务器订阅。基础 URL、主题和只读账号与前面的参数表相同;认证入口可能位于新增订阅流程或设置中的用户配置,具体以客户端版本为准。
iOS 需要额外检查服务端配置:
base-url: "https://ntfy.example.com"upstream-base-url: "https://ntfy.sh"官方 iOS App 的后台即时通知需要通过上游唤醒,再从自建服务器取回消息。公网 base-url 必须与客户端订阅地址一致,手机也必须能访问该地址;配置一个只在家中局域网可用的名字,离开局域网后并不会自动可达。
按官方说明,上游请求携带原消息 ID 和主题完整 URL 的 SHA-256 校验值,不转发实际消息正文。它仍然依赖外部上游以及 Apple 的推送链路,不能描述成完全离线、完全无第三方依赖的自托管推送。
因此验收应分两次:先确认 App 打开时能读取,再退到后台或锁屏发送新消息。前台能看到历史记录,不代表后台唤醒链路已经配置正确。
桌面 Web 与 PWA
桌面端不必额外安装一个原生客户端,可以直接打开自建实例的 Web 页面:
https://ntfy.example.com/app登录 notify-reader,订阅 blog-updates,并允许浏览器和操作系统的通知权限。若浏览器支持,可以通过安装应用或添加到主屏幕,把它作为 PWA 使用。
普通网页订阅与后台推送要分开理解:未启用后台通知时,需要保留活动的 ntfy 页面;关闭标签页后的通知依赖 Web Push,不是添加到桌面就自动完成服务端配置。
如需 Web Push,可在受控终端生成 VAPID 密钥。输出中的私钥属于敏感配置,不要提交到仓库:
docker compose -f /srv/ntfy/compose.yml exec ntfy ntfy webpush keys把生成的值合并到 server.yml,以下均为占位符:
web-push-public-key: "<GENERATED_PUBLIC_KEY>"web-push-private-key: "<GENERATED_PRIVATE_KEY>"web-push-file: "/var/lib/ntfy/webpush.db"web-push-email-address: "admin@example.com"配置后重启 ntfy,并在自建实例的 Web 应用里启用后台通知。保存好私钥和 webpush.db,不要在每次重建容器时重新生成密钥。此时 server.yml 也包含私钥,应限制宿主文件权限,同时确保容器能读取。
后台通知仅支持 Web 应用所在的同一服务器,所以应从自建域名打开 Web 应用,而不是在 ntfy.sh 网页里配置另一台服务器后就期待后台投递。浏览器完全退出后能否通知还取决于平台;iOS 的 PWA 另有系统版本和添加到主屏幕的要求,不能与原生 App 的上游配置混为一谈。
先验证读权限,再发布一条测试通知
客户端安装后,先用命令行区分认证失败和推送失败。以下 curl 命令仅提供用户名,curl 会提示输入密码,避免把明文密码写入命令历史。
读取缓存中的消息,空结果也可能只是主题尚无消息:
curl --fail-with-body -sS --user notify-reader \ 'https://ntfy.example.com/blog-updates/json?poll=1'也可以保持一个实时订阅,按 Ctrl+C 结束:
curl --fail-with-body -N --user notify-reader \ 'https://ntfy.example.com/blog-updates/json'下一条命令会向真实主题发布消息,应使用专门的测试主题或明确标注测试内容:
curl --fail-with-body -sS --user blog-reminder \ -H 'Content-Type: text/plain; charset=utf-8' \ -H 'Title: ntfy connection test' \ -H 'Priority: 3' \ --data-binary 'ntfy 联调测试:确认手机和桌面都能收到。' \ 'https://ntfy.example.com/blog-updates'发布接口返回成功,只能证明 ntfy 接受了请求。完整验收还要观察订阅连接是否收到事件、客户端是否显示消息、后台是否弹出通知。
同时检查最小权限是否有效:匿名读取不应成功,notify-reader 发布不应成功,blog-reminder 读取也不应成功。不要使用管理员账号测试后就认定普通客户端的 ACL 正确。
参考 Alertmanager 转发脚本时,注意 URL 的含义
参考部署仓库中的 alertmanager-ntfy 目录包含:
pigsty-alert-ntfy.py:接收 Alertmanager 请求并发布到 ntfy,同时包含可选 Bugsink 转发入口。pigsty-alert-audit.service:以prometheus用户运行脚本,从/etc/infra-alerts/ntfy.env加载配置。
这不是 ntfy 服务端,它是一个发布端适配器。Alertmanager 的 webhook 发往适配器,适配器整理消息,再带 Bearer Token 调用 ntfy。
脚本的发布配置拆成三个变量:
NTFY_URL=https://ntfy.example.comNTFY_TOPIC=blog-updatesNTFY_TOKEN=<PUBLISHER_TOKEN>脚本内部将地址拼接为 NTFY_URL/NTFY_TOPIC。相比之下,博客催更服务的配置是:
NTFY_URL=http://ntfy/blog-updatesNTFY_TOKEN=<BLOG_REMINDER_TOKEN>同名环境变量在两个程序里含义不同。 把催更服务的完整主题 URL 直接填进转发脚本,会重复拼接主题;把转发脚本的基础 URL 直接填进催更服务,则缺少目标主题。
转发脚本会把 Alertmanager 的 commonLabels、commonAnnotations 和告警数量整理成正文:恢复消息优先级为 3,crit 或 critical 告警为 5,其他未恢复告警为 4。标题经过编码后放入 HTTP 头,正文按 UTF-8 发送,网络超时为 10 秒。这些是参考脚本的规则,不是 ntfy 自动提供的业务语义。
Alertmanager 接收端绑定在 127.0.0.1:9099,路径是 /alerts,没有单独的请求认证,安全前提是只允许可信本机调用。Alertmanager 如果运行在另一个容器里,127.0.0.1 指向的是它自己的容器,需要重新设计可达路径和访问控制,不能只把监听地址改成 0.0.0.0 后开放公网。
参考脚本没有博客的 IP 12 小时去重逻辑。ntfy 发布失败时,适配器会返回 502;Alertmanager 后续重试是否造成重复,应按告警链路单独设计,不能因为两者使用同一个 ntfy 就认为去重行为相同。
手机只需订阅最终的 ntfy 服务地址和主题,不需要配置 Alertmanager 的 /alerts,也不需要知道脚本的监听端口。真实运维中可以让告警与催更使用不同主题和独立发布账号,避免紧急告警被普通消息淹没。
如何接收本站催更通知
博客按钮到 ntfy 的实现见 给静态博客加一个催更按钮:ntfy 接入、IP 去重与 Docker 部署。
已经有 ntfy 服务时,客户端只需要完成三件事:
- 填写该服务对外的 HTTPS 基础 URL,不使用发布程序的 Docker 内网地址。
- 订阅发布程序实际使用的主题:催更程序取完整
NTFY_URL中的主题,Alertmanager 转发脚本取NTFY_TOPIC。 - 使用对该主题有
read-only权限的账号登录,允许通知,并做一次前台及后台测试。
本站催更沿用了已有通知主题,示例中的 blog-updates 不是对真实主题名的披露。已有订阅账号若能读取实际主题,就不需要为催更重复建账号或另订一个示例主题。
常见故障按层排查
| 现象 | 优先检查 |
|---|---|
| 无法连接 | 域名解析、公网端口、TLS 证书链、客户端是否误填 Docker 名称 |
| 返回登录页 HTML | 入口网关是否把 ntfy API 重定向到交互式登录页面 |
| 401 / 403 | 账号属于哪台服务器、密码或 Token 是否有效、是否拥有目标主题读写权限 |
| 只写 Token 能发布但无法订阅 | 这是预期 ACL 行为,改用只读订阅账号,不要扩大发布账号权限 |
| Web 页面可开但订阅断开 | WebSocket 转发、代理缓冲、长连接超时 |
| Android 熄屏后延迟 | 即时投递、前台订阅服务、电池优化及系统后台限制 |
| iOS 前台正常、后台无通知 | base-url 是否匹配、upstream-base-url、上游可达性、系统通知设置 |
| 关闭网页后不通知 | Web Push 是否配置,是否从自建实例打开 Web 应用,以及浏览器平台限制 |
| 重启后用户或缓存丢失 | auth-file、cache-file 所在目录是否真的持久化 |
| 数据库写入失败 | 挂载目录权限、磁盘剩余空间,是否错误地挂成只读 |
维护时应备份配置及认证数据,对正在使用的 SQLite 采用一致性备份方式;不要只复制活动数据库的主文件而忽略 WAL。升级前保留旧镜像版本和数据备份,不要无条件跟随 latest,也不要把清空数据目录当成排障第一步。
本文核对了参考转发脚本和官方文档,没有重新部署该目录中的服务,也没有在实体 Android 或 iOS 设备上逐项实测菜单。最终验收应以目标设备在实际网络中的收取结果为准。
参考资料
- ntfy 服务端配置、访问控制与代理设置
- iOS 即时通知与上游机制
- Web Push 服务端配置
- 官方 Android / iOS 客户端说明
- Web 客户端与后台通知限制
- 本地参考:
docker-deploy/alertmanager-ntfy/pigsty-alert-ntfy.py、docker-deploy/alertmanager-ntfy/pigsty-alert-audit.service。