通知先を後回しにしてきた人へ
バックアップや監視の仕組みを作り始めたものの、「失敗したときにどう受け取るか」だけが残っている、という方は少なくないと思います。メールでも受け取れますが、もう少し軽い手段が欲しい。そこで候補に挙がりやすいのがセルフホスト型の通知サービス ntfy です。
ただ、通知基盤の運用差が出るのは「届いた瞬間」ではありません。届かないとき、誰でも送れてしまうとき、再起動や再作成で設定が消えたときに差が出ます。ですからこの記事では、ntfyを紹介して終わりにせず、使い捨てのDocker環境で小さく起動し、意図的に壊して観測します。
この記事が答えるのは、家庭ラボ運用者が抱える次の問いです。「ntfyをセルフホスト通知として採用する前に、どの失敗条件を先に確認しておくべきか」。比較対象として念頭に置くのは、メール通知と、監視ツール内蔵の通知機能です。
検証環境
検証はある使い捨てVM(某所ラボの一時的なDocker実行環境)で行い、終了後はコンテナ・ボリューム・一時ディレクトリをすべて破棄しています。本番運用そのものではなく、壊れ方を観測するための環境です。
- Docker version 29.6.0
- Docker Compose version v5.2.0
- ntfy 2.25.0(コンテナイメージ binwiederhier/ntfy:latest)
- 待ち受けはローカルホストのみ(127.0.0.1:8080 をコンテナ内の80へ)
本記事のコマンド・応答はこの環境で実際に取得したものを、公開可能な形に整えて掲載しています。
結論(先に)
- 無認証の最小構成なら、curlだけで送信も購読もすぐにできます。ただし「誰でも送れる」状態でもあります。
- 認証を deny-all にすると、資格情報なしの送信は 403(code 40301)で弾かれます。正しい資格情報を付ければ 200 に戻ります。
- 誤ったtopicを購読してもエラーにはならず、pollは 200 で空が返ります。「届かない」のとエラーは別物です。
- 設定ミス(無効な値)はサーバ起動時に落ちます。送信前に気づける失敗です。
- 永続化ボリュームを消すと、作成済みユーザが失われ、同じ資格情報でも 401 に変わります。
常用するなら、無認証で始めないこと、永続ボリュームを必ず設計すること、この2点が出発点になります。
1. 無認証の最小構成で起動する
まずは認証なしのComposeを置きます。編集に使ったエディタコマンドは次のとおりです。
# vi /tmp/lab/docker-compose.yml
ファイルの内容は次のとおりです(認証設定はまだありません)。
services:
ntfy:
image: binwiederhier/ntfy:latest
command: serve
environment:
- 設定項目=http://127.0.0.1:8080
- 設定項目=:80
ports:
- "127.0.0.1:8080:80"
volumes:
- ntfy-cache:/var/cache/ntfy
- ntfy-etc:/etc/ntfy
volumes:
ntfy-cache:
ntfy-etc:
起動して状態を確認します。
# docker compose -f /tmp/lab/docker-compose.yml up -d
# docker compose -f /tmp/lab/docker-compose.yml ps
設定項目 設定項目 設定項目 設定項目 設定項目
lab-ntfy-1 binwiederhier/ntfy:latest ntfy Up 5 seconds 127.0.0.1:8080->80/tcp
ntfy-1 | 設定項目 Listening on :80[http], ntfy 2.25.0, log level is 設定項目 (tag=startup)
2. curlだけで送受信できる(=誰でも送れる)
認証がない状態では、送信も購読もcurlだけで通ります。
# curl -s -d 'test message' http://127.0.0.1:8080/labtest
# curl -s 'http://127.0.0.1:8080/labtest/json?poll=1'
{"id":"s9joqv2195eO","event":"message","topic":"labtest","message":"test message"}
{"id":"s9joqv2195eO","event":"message","topic":"labtest","message":"test message"}
手軽さの裏返しとして、URLとtopic名さえ分かれば誰でも投稿できます。家庭ラボでも、待ち受けをローカルに閉じる、あるいは認証を入れる、のどちらかは必要だと分かります。
3. 認証(deny-all)に切り替え、資格情報なしを403で観測する
既定アクセスを deny-all にし、ユーザを追加します。Composeに認証用の環境変数とボリュームを足します。
services:
ntfy:
image: binwiederhier/ntfy:latest
command: serve
environment:
- 設定項目=http://127.0.0.1:8080
- 設定項目=:80
- 設定項目=/var/lib/ntfy/user.db
- 設定項目=deny-all
ports:
- "127.0.0.1:8080:80"
volumes:
- ntfy-cache:/var/cache/ntfy
- ntfy-etc:/etc/ntfy
- ntfy-lib:/var/lib/ntfy
volumes:
ntfy-cache:
ntfy-etc:
ntfy-lib:
再作成してユーザを追加し、資格情報なしで送ってみます。
# docker compose -f /tmp/lab/docker-compose.yml up -d --force-recreate
# docker compose -f /tmp/lab/docker-compose.yml exec -T -e 設定項目=example-pass ntfy ntfy user add --role=admin labuser
# curl -i -s -d 'msg' http://127.0.0.1:8080/labtest
user labuser added with role admin
設定項目/1.1 403 Forbidden
Content-Type: application/json
{"code":40301,"http":403,"error":"forbidden","link":"https://ntfy.sh/docs/publish/#authentication"}
資格情報のない送信は 403、code 40301 として弾かれました。応答にドキュメントのリンクが含まれるので、調査の入口は分かりやすい部類です。
4. 正しい資格情報なら200に戻る
# curl -s -o /dev/null -w 'auth-ok:%{http_code}\n' -u labuser:example-pass -d 'auth ok' http://127.0.0.1:8080/labtest
auth-ok:200
認証を入れても、正しい資格情報を付ければそのまま送れます。
5. 誤topicは「エラー」ではなく「空」になる
ありがちな失敗のひとつが、購読側のtopic名を間違えることです。これはエラーになりません。
# curl -s -o /dev/null -w 'wrongtopic:%{http_code}\n' -u labuser:example-pass 'http://127.0.0.1:8080/nonexistent-topic/json?poll=1'
wrongtopic:200
ステータスは 200 で、ただ中身が空のまま返ります。「届かない」原因が送信側ではなくtopic名の食い違いであるケースは、エラーが出ないぶん気づきにくいということです。
6. 設定ミスは起動時に落ちる
無効な設定値を入れたComposeを単発で起動すると、サーバが立ち上がりません。
services:
ntfybad:
image: binwiederhier/ntfy:latest
command: serve
environment:
- 設定項目=not-a-duration
# docker compose -f /tmp/lab/bad-compose.yml run --rm ntfybad serve
invalid cache duration: not-a-duration
この種の失敗は送信前に起動段階で表面化するので、むしろ気づきやすい失敗です。
7. ボリュームを消すと401になり、ユーザは消える
最後に、永続化なしで状態が消える挙動を確認します。ボリュームごと削除して再起動し、同じ資格情報で送ってみます。
# docker compose -f /tmp/lab/docker-compose.yml down -v
# docker compose -f /tmp/lab/docker-compose.yml up -d
# curl -s -o /dev/null -w 'after-wipe:%{http_code}\n' -u labuser:example-pass -d 'after wipe' http://127.0.0.1:8080/labtest
# docker compose -f /tmp/lab/docker-compose.yml exec -T ntfy ntfy user list
after-wipe:401
user * (role: anonymous, tier: none)
- no topic-specific permissions
- no access to any (other) topics (server config)
さきほど 200 だった同じ資格情報が 401 になりました。ユーザ一覧も anonymous だけに戻っています。403(アクセス拒否)と 401(資格情報そのものが通らない)の違いがここで効いてきます。-v 付きの down はボリュームを消すため、認証DBを永続ボリュームに置いていなければユーザは消えます。
8. 後始末
検証後はコンテナ・ボリューム・一時ディレクトリを破棄しました。
# docker compose -f /tmp/lab/docker-compose.yml down -v
# docker ps -a
# docker volume ls
残存コンテナ・残存ボリュームともにない状態で終えています。
メール・監視内蔵通知との比較
| 観点 | ntfy(セルフホスト) | メール通知 | 監視ツール内蔵通知 |
|---|---|---|---|
| 構築の軽さ | Docker 1サービスで起動可 | 既存メールがあれば追加不要 | ツール側に同梱 |
| 認証の分かりやすさ | deny-all+ユーザ追加。明示的だが要設定 | 送信側設定に依存 | ツール設定に従う |
| 届かない時の調査 | 応答コードとログで切り分けやすい | 配送遅延・迷惑判定で不透明になりがち | ツール依存 |
| 永続化への強さ | ボリューム設計を誤ると状態喪失 | サーバ側が保持 | ツール側が保持 |
メールは既存資産で始められる一方、遅延や迷惑メール判定で「届かない理由」が見えにくくなりがちです。監視ツール内蔵の通知はそのツール内で完結する手軽さがあります。ntfyは、自分で応答コードとログを見て切り分けられる代わりに、認証と永続化を自分で設計する責任が伴う、という整理になります。
採用してよい条件・まだ採用しない方がよい条件
採用に進んでよいのは、次を満たせるときです。
- 最初から認証(deny-all)を入れ、無認証のまま公開しない
- 認証DBとキャッシュを永続ボリュームに置き、再作成しても消えない設計にする
- 「届かない」を、送信エラー・topic名違い・購読側の問題に切り分ける手順を持つ
次の条件が未整理なら、もう少し検証を続けてからにする方が安全です。
- TLSやリバースプロキシ越しの公開設計が未定
- 本番でのアクセス制御の粒度(topicごとの権限)が未設計
- 永続ボリュームのバックアップ方針がない
制約・再現条件
- 本検証は使い捨てVM内のDockerで、待ち受けをローカルホストに限定して実施しました。TLS終端やリバースプロキシ越しの挙動、外部公開時のアクセス制御の粒度は対象外です。これらは本番設計時に別途確認してください。
- 検証中、一時VMへの接続時にホスト鍵を既知ホストへ追加する旨の警告が一度出ましたが、ntfyの動作そのものには影響していません。
- 再現する場合は、同等のDocker実行環境で本記事のComposeとコマンドをそのまま流せば、同じ応答コードの並び(無認証200→資格情報なし403→正資格200→誤topic200空→設定ミス起動失敗→ボリューム削除後401)を観測できます。
通知は、届くときより届かないときに本質が出ます。常用の前に、この壊れ方を一度自分の手で見ておくと、後の運用がだいぶ静かになります。
この検証を回している環境
この検証は、自宅の常設ラボ(使い捨てVM/LXCを回す母艦+GPU+VLAN分離ネットワーク)で動かしています。使っている機材と選定理由、全体構成は1本にまとめています。
ラボ構成のまとめを見る →