Files
homey/modules/services/ntfy.nix
T
2026-05-10 13:44:27 +03:00

170 lines
6.4 KiB
Nix

{ config, lib, pkgs, homeyConfig, ... }:
# Ntfy — self-hosted push notification server.
#
# Mobile app (Android/iOS) connects to https://ntfy.zakobar.com with a token
# and subscribes to the "alerts" topic. Uptime Kuma and Grafana send alerts
# to that topic when services go down.
#
# Auth model:
# - Web UI: public-facing but ntfy enforces its own auth (deny-all by default)
# - Caddy does NOT put forward_auth here; ntfy has native token/password auth
# so the mobile app can connect without Authelia SSO complications.
#
# Setup after first deploy:
# 1. Visit https://ntfy.zakobar.com — log in with the admin password from sops.
# 2. Create an access token for your phone (Admin → Users & Tokens).
# 3. In the Ntfy app: server = https://ntfy.zakobar.com, token = <your-token>.
# 4. Subscribe to the "alerts" topic.
#
# Volume layout:
# <dataDir>/ntfy/auth.db ← user/token database
# <dataDir>/ntfy/cache.db ← message cache (for missed messages)
# <dataDir>/ntfy/attachments/ ← file attachments
#
# Secrets consumed from sops:
# ntfy/admin_password
let
cfg = config.homey.ntfy;
dataDir = config.homey.storage.mountPoint;
domain = homeyConfig.domain;
in
{
options.homey.ntfy = {
enable = lib.mkEnableOption "Ntfy push notification server";
port = lib.mkOption {
type = lib.types.port;
default = 2586;
description = "Host port ntfy listens on (bound to 127.0.0.1).";
};
};
config = lib.mkIf cfg.enable {
# -----------------------------------------------------------------------
# Secrets
# -----------------------------------------------------------------------
sops.secrets."ntfy/admin_password" = { owner = "root"; };
# -----------------------------------------------------------------------
# ntfy-sh native NixOS service
# -----------------------------------------------------------------------
services.ntfy-sh = {
enable = true;
settings = {
# Bind to localhost; Caddy reverse-proxies it
listen-http = "127.0.0.1:${toString cfg.port}";
base-url = "https://ntfy.${domain}";
# Require auth on all topics — deny unauthenticated access entirely
auth-default-access = "deny-all";
# Persistent state on external HD
auth-file = "${dataDir}/ntfy/auth.db";
cache-file = "${dataDir}/ntfy/cache.db";
attachment-root = "${dataDir}/ntfy/attachments";
# Keep messages for 12 hours so the app catches up if offline
cache-duration = "12h";
# Attachment limits
attachment-total-size-limit = "5G";
attachment-file-size-limit = "15M";
attachment-expiry-duration = "3h";
};
};
# Minimal config for the `ntfy user` CLI — the NixOS module puts its
# generated config in the nix store under an unpredictable path, so we
# write a separate file just containing the auth-file path. The server
# ignores this file (it uses the module-generated one via -c flag).
environment.etc."ntfy-sh/user-cli.yml" = {
text = "auth-file: ${dataDir}/ntfy/auth.db\n";
mode = "0444";
};
# Create ntfy data directories on the external HD before ntfy starts.
# Runs as a separate root service (outside ntfy-sh's restricted namespace)
# so it can access /mnt/data without hitting ReadWritePaths restrictions.
systemd.services.ntfy-sh-mkdir = {
description = "Create Ntfy data directories on external HD";
wantedBy = [ "ntfy-sh.service" ];
before = [ "ntfy-sh.service" ];
after = [ "mnt-data.mount" ];
requires = [ "mnt-data.mount" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "ntfy-mkdir" ''
set -euo pipefail
mkdir -p ${dataDir}/ntfy/attachments
chown -R ntfy-sh:ntfy-sh ${dataDir}/ntfy
chmod 0750 ${dataDir}/ntfy ${dataDir}/ntfy/attachments
'';
};
};
# Ensure ntfy-sh starts after the HD is mounted and dirs are ready.
# Also widen ReadWritePaths so ntfy-sh can write to the external HD path
# (the NixOS module restricts writes to /var/lib/ntfy-sh by default).
systemd.services.ntfy-sh = {
after = lib.mkAfter [ "mnt-data.mount" "ntfy-sh-mkdir.service" ];
requires = lib.mkAfter [ "mnt-data.mount" "ntfy-sh-mkdir.service" ];
serviceConfig.ReadWritePaths = lib.mkAfter [ "${dataDir}/ntfy" ];
};
# -----------------------------------------------------------------------
# Create the admin user on first start (idempotent)
# -----------------------------------------------------------------------
systemd.services.ntfy-sh-setup = {
description = "Create Ntfy admin user";
wantedBy = [ "multi-user.target" ];
after = [ "ntfy-sh.service" "mnt-data.mount" ];
requires = [ "ntfy-sh.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
LoadCredential = "ntfy_admin_password:${config.sops.secrets."ntfy/admin_password".path}";
ExecStart = pkgs.writeShellScript "ntfy-create-admin" ''
set -euo pipefail
# Wait until ntfy HTTP endpoint is ready (max 60 s)
for i in $(seq 1 30); do
if ${pkgs.curl}/bin/curl -sf http://127.0.0.1:${toString cfg.port}/v1/health > /dev/null 2>&1; then
break
fi
sleep 2
done
PASS=$(cat "$CREDENTIALS_DIRECTORY/ntfy_admin_password")
# Use the minimal CLI config (just has auth-file path).
NTFY="${pkgs.ntfy-sh}/bin/ntfy user --config /etc/ntfy-sh/user-cli.yml"
# ntfy user list outputs a Unicode table; grep for admin in it.
# ntfy user add reads password + confirmation from stdin (two lines).
if $NTFY list 2>/dev/null | grep -qE "admin"; then
echo "ntfy-sh-setup: admin user already exists"
else
printf '%s\n%s\n' "$PASS" "$PASS" | $NTFY add --role=admin admin
echo "ntfy-sh-setup: admin user created"
fi
'';
};
};
# -----------------------------------------------------------------------
# Uptime Kuma monitor for this service
# -----------------------------------------------------------------------
homey.monitoring.monitors = [{
name = "Ntfy";
url = "https://ntfy.${domain}/v1/health";
interval = 60;
}];
};
}