diff --git a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
index d721fb5dd83b..b4862254f628 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
@@ -232,6 +232,13 @@
services.outline.
+
+
+ ntfy.sh, a push
+ notification service. Available as
+ services.ntfy-sh
+
+
alps,
diff --git a/nixos/doc/manual/release-notes/rl-2211.section.md b/nixos/doc/manual/release-notes/rl-2211.section.md
index 53f26c4ccc21..5a661a42ae60 100644
--- a/nixos/doc/manual/release-notes/rl-2211.section.md
+++ b/nixos/doc/manual/release-notes/rl-2211.section.md
@@ -85,6 +85,8 @@ In addition to numerous new and upgraded packages, this release has the followin
- [Outline](https://www.getoutline.com/), a wiki and knowledge base similar to Notion. Available as [services.outline](#opt-services.outline.enable).
+- [ntfy.sh](https://ntfy.sh), a push notification service. Available as [services.ntfy-sh](#opt-services.ntfy-sh.enable)
+
- [alps](https://git.sr.ht/~migadu/alps), a simple and extensible webmail. Available as [services.alps](#opt-services.alps.enable).
- [netbird](https://netbird.io), a zero configuration VPN.
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 998919f2a43a..9ea3a077ef8a 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -609,6 +609,7 @@
./services/misc/nix-optimise.nix
./services/misc/nix-ssh-serve.nix
./services/misc/novacomd.nix
+ ./services/misc/ntfy-sh.nix
./services/misc/nzbget.nix
./services/misc/nzbhydra2.nix
./services/misc/octoprint.nix
diff --git a/nixos/modules/services/misc/ntfy-sh.nix b/nixos/modules/services/misc/ntfy-sh.nix
new file mode 100644
index 000000000000..9d52fcf25364
--- /dev/null
+++ b/nixos/modules/services/misc/ntfy-sh.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.ntfy-sh;
+
+ settingsFormat = pkgs.formats.yaml { };
+in
+
+{
+ options.services.ntfy-sh = {
+ enable = mkEnableOption (mdDoc "[ntfy-sh](https://ntfy.sh), a push notification service");
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.ntfy-sh;
+ defaultText = literalExpression "pkgs.ntfy-sh";
+ description = mdDoc "The ntfy.sh package to use.";
+ };
+
+ user = mkOption {
+ default = "ntfy-sh";
+ type = types.str;
+ description = lib.mdDoc "User the ntfy-sh server runs under.";
+ };
+
+ group = mkOption {
+ default = "ntfy-sh";
+ type = types.str;
+ description = lib.mdDoc "Primary group of ntfy-sh user.";
+ };
+
+ settings = mkOption {
+ type = types.submodule { freeformType = settingsFormat.type; };
+
+ default = { };
+
+ example = literalExpression ''
+ {
+ listen-http = ":8080";
+ }
+ '';
+
+ description = mdDoc ''
+ Configuration for ntfy.sh, supported values are [here](https://ntfy.sh/docs/config/#config-options).
+ '';
+ };
+ };
+
+ config =
+ let
+ configuration = settingsFormat.generate "server.yml" cfg.settings;
+ in
+ mkIf cfg.enable {
+ # to configure access control via the cli
+ environment = {
+ etc."ntfy/server.yml".source = configuration;
+ systemPackages = [ cfg.package ];
+ };
+
+ systemd.services.ntfy-sh = {
+ description = "Push notifications server";
+
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+
+ serviceConfig = {
+ ExecStart = "${cfg.package}/bin/ntfy serve -c ${configuration}";
+ User = cfg.user;
+
+ AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+ PrivateTmp = true;
+ NoNewPrivileges = true;
+ CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
+ ProtectSystem = "full";
+ ProtectKernelTunables = true;
+ ProtectKernelModules = true;
+ ProtectKernelLogs = true;
+ ProtectControlGroups = true;
+ PrivateDevices = true;
+ RestrictSUIDSGID = true;
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ MemoryDenyWriteExecute = true;
+ };
+ };
+
+ users.groups = optionalAttrs (cfg.group == "ntfy-sh") {
+ ntfy-sh = { };
+ };
+
+ users.users = optionalAttrs (cfg.user == "ntfy-sh") {
+ ntfy-sh = {
+ isSystemUser = true;
+ group = cfg.group;
+ };
+ };
+ };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index e0121fe6b6b8..0c7ef4a0f2eb 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -400,6 +400,7 @@ in {
noto-fonts = handleTest ./noto-fonts.nix {};
novacomd = handleTestOn ["x86_64-linux"] ./novacomd.nix {};
nsd = handleTest ./nsd.nix {};
+ ntfy-sh = handleTest ./ntfy-sh.nix {};
nzbget = handleTest ./nzbget.nix {};
nzbhydra2 = handleTest ./nzbhydra2.nix {};
oh-my-zsh = handleTest ./oh-my-zsh.nix {};
diff --git a/nixos/tests/ntfy-sh.nix b/nixos/tests/ntfy-sh.nix
new file mode 100644
index 000000000000..c0c289b904b6
--- /dev/null
+++ b/nixos/tests/ntfy-sh.nix
@@ -0,0 +1,20 @@
+import ./make-test-python.nix {
+
+ nodes.machine = { ... }: {
+ services.ntfy-sh.enable = true;
+ };
+
+ testScript = ''
+ import json
+
+ msg = "Test notification"
+
+ machine.wait_for_unit("multi-user.target")
+
+ machine.succeed(f"curl -d '{msg}' localhost:80/test")
+
+ notif = json.loads(machine.succeed("curl -s localhost:80/test/json?poll=1"))
+
+ assert msg == notif["message"], "Wrong message"
+ '';
+}