1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 inherit (lib)
10 mkEnableOption
11 mkPackageOption
12 mkOption
13 maintainers
14 ;
15 inherit (lib.types)
16 bool
17 port
18 str
19 submodule
20 ;
21 cfg = config.services.navidrome;
22 settingsFormat = pkgs.formats.json { };
23in
24{
25 options = {
26 services.navidrome = {
27
28 enable = mkEnableOption "Navidrome music server";
29
30 package = mkPackageOption pkgs "navidrome" { };
31
32 settings = mkOption {
33 type = submodule {
34 freeformType = settingsFormat.type;
35
36 options = {
37 Address = mkOption {
38 default = "127.0.0.1";
39 description = "Address to run Navidrome on.";
40 type = str;
41 };
42
43 Port = mkOption {
44 default = 4533;
45 description = "Port to run Navidrome on.";
46 type = port;
47 };
48
49 EnableInsightsCollector = mkOption {
50 default = false;
51 description = "Enable anonymous usage data collection, see <https://www.navidrome.org/docs/getting-started/insights/> for details.";
52 type = bool;
53 };
54 };
55 };
56 default = { };
57 example = {
58 MusicFolder = "/mnt/music";
59 };
60 description = "Configuration for Navidrome, see <https://www.navidrome.org/docs/usage/configuration-options/> for supported values.";
61 };
62
63 user = mkOption {
64 type = str;
65 default = "navidrome";
66 description = "User under which Navidrome runs.";
67 };
68
69 group = mkOption {
70 type = str;
71 default = "navidrome";
72 description = "Group under which Navidrome runs.";
73 };
74
75 openFirewall = mkOption {
76 type = bool;
77 default = false;
78 description = "Whether to open the TCP port in the firewall";
79 };
80
81 environmentFile = lib.mkOption {
82 type = lib.types.nullOr lib.types.path;
83 default = null;
84 description = "Environment file, used to set any secret ND_* environment variables.";
85 };
86 };
87 };
88
89 config =
90 let
91 inherit (lib) mkIf optional getExe;
92 WorkingDirectory = "/var/lib/navidrome";
93 in
94 mkIf cfg.enable {
95 systemd = {
96 tmpfiles.settings.navidromeDirs = {
97 "${cfg.settings.DataFolder or WorkingDirectory}"."d" = {
98 mode = "700";
99 inherit (cfg) user group;
100 };
101 "${cfg.settings.CacheFolder or (WorkingDirectory + "/cache")}"."d" = {
102 mode = "700";
103 inherit (cfg) user group;
104 };
105 "${cfg.settings.MusicFolder or (WorkingDirectory + "/music")}"."d" = {
106 mode = ":700";
107 user = ":${cfg.user}";
108 group = ":${cfg.group}";
109 };
110 };
111 services.navidrome = {
112 description = "Navidrome Media Server";
113 after = [ "network.target" ];
114 wantedBy = [ "multi-user.target" ];
115 serviceConfig = {
116 ExecStart = ''
117 ${getExe cfg.package} --configfile ${settingsFormat.generate "navidrome.json" cfg.settings}
118 '';
119 EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
120 User = cfg.user;
121 Group = cfg.group;
122 StateDirectory = "navidrome";
123 inherit WorkingDirectory;
124 RuntimeDirectory = "navidrome";
125 RootDirectory = "/run/navidrome";
126 ReadWritePaths = "";
127 BindPaths =
128 optional (cfg.settings ? DataFolder) cfg.settings.DataFolder
129 ++ optional (cfg.settings ? CacheFolder) cfg.settings.CacheFolder;
130 BindReadOnlyPaths =
131 [
132 # navidrome uses online services to download additional album metadata / covers
133 "${config.security.pki.caBundle}:/etc/ssl/certs/ca-certificates.crt"
134 builtins.storeDir
135 "/etc"
136 ]
137 ++ optional (cfg.settings ? MusicFolder) cfg.settings.MusicFolder
138 ++ lib.optionals config.services.resolved.enable [
139 "/run/systemd/resolve/stub-resolv.conf"
140 "/run/systemd/resolve/resolv.conf"
141 ];
142 CapabilityBoundingSet = "";
143 RestrictAddressFamilies = [
144 "AF_UNIX"
145 "AF_INET"
146 "AF_INET6"
147 ];
148 RestrictNamespaces = true;
149 PrivateDevices = true;
150 PrivateUsers = true;
151 ProtectClock = true;
152 ProtectControlGroups = true;
153 ProtectHome = true;
154 ProtectKernelLogs = true;
155 ProtectKernelModules = true;
156 ProtectKernelTunables = true;
157 SystemCallArchitectures = "native";
158 SystemCallFilter = [
159 "@system-service"
160 "~@privileged"
161 ];
162 RestrictRealtime = true;
163 LockPersonality = true;
164 MemoryDenyWriteExecute = true;
165 UMask = "0066";
166 ProtectHostname = true;
167 };
168 };
169 };
170
171 users.users = mkIf (cfg.user == "navidrome") {
172 navidrome = {
173 inherit (cfg) group;
174 isSystemUser = true;
175 };
176 };
177
178 users.groups = mkIf (cfg.group == "navidrome") { navidrome = { }; };
179
180 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.settings.Port ];
181 };
182 meta.maintainers = with maintainers; [ fsnkty ];
183}