1 { config, lib, pkgs, ... }:
5 cfg = config.services.freshrss;
7 extension-env = pkgs.buildEnv {
8 name = "freshrss-extensions";
9 paths = cfg.extensions;
12 DATA_PATH = cfg.dataDir;
13 THIRDPARTY_EXTENSIONS_PATH = "${extension-env}/share/freshrss/";
17 meta.maintainers = with maintainers; [ etu stunkymonkey mattchrist ];
19 options.services.freshrss = {
20 enable = mkEnableOption "FreshRSS RSS aggregator and reader with php-fpm backend";
22 package = mkPackageOption pkgs "freshrss" { };
24 extensions = mkOption {
25 type = types.listOf types.package;
27 defaultText = literalExpression "[]";
28 example = literalExpression ''
29 with freshrss-extensions; [
32 (freshrss-extensions.buildFreshRssExtension {
33 FreshRssExtUniqueId = "ReadingTime";
34 pname = "reading-time";
36 src = pkgs.fetchFromGitLab {
37 domain = "framagit.org";
39 repo = "FreshRSS_Extension-ReadingTime";
40 rev = "fb6e9e944ef6c5299fa56ffddbe04c41e5a34ebf";
41 hash = "sha256-C5cRfaphx4Qz2xg2z+v5qRji8WVSIpvzMbethTdSqsk=";
46 description = "Additional extensions to be used.";
49 defaultUser = mkOption {
52 description = "Default username for FreshRSS.";
56 passwordFile = mkOption {
57 type = types.nullOr types.path;
59 description = "Password for the defaultUser for FreshRSS.";
60 example = "/run/secrets/freshrss";
65 description = "Default URL for FreshRSS.";
66 example = "https://freshrss.example.com";
72 description = "Default language for FreshRSS.";
78 type = types.enum [ "sqlite" "pgsql" "mysql" ];
80 description = "Database type.";
85 type = types.nullOr types.str;
86 default = "localhost";
87 description = "Database host for FreshRSS.";
91 type = types.nullOr types.port;
93 description = "Database port for FreshRSS.";
98 type = types.nullOr types.str;
100 description = "Database user for FreshRSS.";
103 passFile = mkOption {
104 type = types.nullOr types.path;
106 description = "Database password file for FreshRSS.";
107 example = "/run/secrets/freshrss";
111 type = types.nullOr types.str;
112 default = "freshrss";
113 description = "Database name for FreshRSS.";
116 tableprefix = mkOption {
117 type = types.nullOr types.str;
119 description = "Database table prefix for FreshRSS.";
120 example = "freshrss";
126 default = "/var/lib/freshrss";
127 description = "Default data folder for FreshRSS.";
128 example = "/mnt/freshrss";
131 virtualHost = mkOption {
132 type = types.nullOr types.str;
133 default = "freshrss";
135 Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
136 You may need to configure the virtualhost further through services.nginx.virtualHosts.<virtualhost>,
137 for example to enable SSL.
142 type = types.nullOr types.str;
143 default = "freshrss";
145 Name of the php-fpm pool to use and setup. If not specified, a pool will be created
152 default = "freshrss";
153 description = "User under which FreshRSS runs.";
156 authType = mkOption {
157 type = types.enum [ "form" "http_auth" "none" ];
159 description = "Authentication type for FreshRSS.";
165 defaultServiceConfig = {
166 ReadWritePaths = "${cfg.dataDir}";
167 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
169 LockPersonality = true;
170 NoNewPrivileges = true;
171 PrivateDevices = true;
176 ProtectControlGroups = true;
178 ProtectHostname = true;
179 ProtectKernelLogs = true;
180 ProtectKernelModules = true;
181 ProtectKernelTunables = true;
182 ProtectProc = "invisible";
183 ProtectSystem = "strict";
185 RestrictNamespaces = true;
186 RestrictRealtime = true;
187 RestrictSUIDSGID = true;
188 SystemCallArchitectures = "native";
189 SystemCallFilter = [ "@system-service" "~@resources" "~@privileged" ];
193 Group = config.users.users.${cfg.user}.group;
194 StateDirectory = "freshrss";
195 WorkingDirectory = cfg.package;
199 assertions = mkIf (cfg.authType == "form") [
201 assertion = cfg.passwordFile != null;
203 `passwordFile` must be supplied when using "form" authentication!
207 # Set up a Nginx virtual host.
208 services.nginx = mkIf (cfg.virtualHost != null) {
210 virtualHosts.${cfg.virtualHost} = {
211 root = "${cfg.package}/p";
214 # this regex is mandatory because of the API
215 locations."~ ^.+?\.php(/.*)?$".extraConfig = ''
216 fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
217 fastcgi_split_path_info ^(.+\.php)(/.*)$;
218 # By default, the variable PATH_INFO is not set under PHP-FPM
219 # But FreshRSS API greader.php need it. If you have a “Bad Request” error, double check this var!
220 # NOTE: the separate $path_info variable is required. For more details, see:
221 # https://trac.nginx.org/nginx/ticket/321
222 set $path_info $fastcgi_path_info;
223 fastcgi_param PATH_INFO $path_info;
224 include ${pkgs.nginx}/conf/fastcgi_params;
225 include ${pkgs.nginx}/conf/fastcgi.conf;
229 tryFiles = "$uri $uri/ index.php";
230 index = "index.php index.html index.htm";
236 services.phpfpm.pools = mkIf (cfg.pool != null) {
240 "listen.owner" = "nginx";
241 "listen.group" = "nginx";
242 "listen.mode" = "0600";
244 "pm.max_children" = 32;
245 "pm.max_requests" = 500;
246 "pm.start_servers" = 2;
247 "pm.min_spare_servers" = 2;
248 "pm.max_spare_servers" = 5;
249 "catch_workers_output" = true;
255 users.users."${cfg.user}" = {
256 description = "FreshRSS service user";
258 group = "${cfg.user}";
261 users.groups."${cfg.user}" = { };
263 systemd.tmpfiles.settings."10-freshrss".${cfg.dataDir}.d = {
265 group = config.users.users.${cfg.user}.group;
268 systemd.services.freshrss-config =
270 settingsFlags = concatStringsSep " \\\n "
271 (mapAttrsToList (k: v: "${k} ${toString v}") {
272 "--default-user" = ''"${cfg.defaultUser}"'';
273 "--auth-type" = ''"${cfg.authType}"'';
274 "--base-url" = ''"${cfg.baseUrl}"'';
275 "--language" = ''"${cfg.language}"'';
276 "--db-type" = ''"${cfg.database.type}"'';
277 # The following attributes are optional depending on the type of
278 # database. Those that evaluate to null on the left hand side
280 ${if cfg.database.name != null then "--db-base" else null} = ''"${cfg.database.name}"'';
281 ${if cfg.database.passFile != null then "--db-password" else null} = ''"$(cat ${cfg.database.passFile})"'';
282 ${if cfg.database.user != null then "--db-user" else null} = ''"${cfg.database.user}"'';
283 ${if cfg.database.tableprefix != null then "--db-prefix" else null} = ''"${cfg.database.tableprefix}"'';
284 # hostname:port e.g. "localhost:5432"
285 ${if cfg.database.host != null && cfg.database.port != null then "--db-host" else null} = ''"${cfg.database.host}:${toString cfg.database.port}"'';
286 # socket path e.g. "/run/postgresql"
287 ${if cfg.database.host != null && cfg.database.port == null then "--db-host" else null} = ''"${cfg.database.host}"'';
291 description = "Set up the state directory for FreshRSS before use";
292 wantedBy = [ "multi-user.target" ];
293 serviceConfig = defaultServiceConfig // {
294 RemainAfterExit = true;
296 restartIfChanged = true;
297 environment = env-vars;
301 userScriptArgs = ''--user ${cfg.defaultUser} ${optionalString (cfg.authType == "form") ''--password "$(cat ${cfg.passwordFile})"''}'';
302 updateUserScript = optionalString (cfg.authType == "form" || cfg.authType == "none") ''
303 ./cli/update-user.php ${userScriptArgs}
305 createUserScript = optionalString (cfg.authType == "form" || cfg.authType == "none") ''
306 ./cli/create-user.php ${userScriptArgs}
310 # do installation or reconfigure
311 if test -f ${cfg.dataDir}/config.php; then
312 # reconfigure with settings
313 ./cli/reconfigure.php ${settingsFlags}
316 # check correct folders in data folder
318 # install with settings
319 ./cli/do-install.php ${settingsFlags}
325 systemd.services.freshrss-updater = {
326 description = "FreshRSS feed updater";
327 after = [ "freshrss-config.service" ];
329 environment = env-vars;
330 serviceConfig = defaultServiceConfig // {
331 ExecStart = "${cfg.package}/app/actualize_script.php";