1 { config, lib, pkgs, ... }:
5 cfg = config.services.tt-rss;
7 inherit (cfg) phpPackage;
11 dbPort = if cfg.database.port == null
12 then (if cfg.database.type == "pgsql" then 5432 else 3306)
13 else cfg.database.port;
17 mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
18 pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
22 if (cfg.database.password != null) then
23 "'${(escape ["'" "\\"] cfg.database.password)}'"
24 else if (cfg.database.passwordFile != null) then
25 "file_get_contents('${cfg.database.passwordFile}')"
29 in pkgs.writeText "config.php" ''
31 putenv('TTRSS_PHP_EXECUTABLE=${phpPackage}/bin/php');
33 putenv('TTRSS_LOCK_DIRECTORY=${cfg.root}/lock');
34 putenv('TTRSS_CACHE_DIR=${cfg.root}/cache');
35 putenv('TTRSS_ICONS_DIR=${cfg.root}/feed-icons');
36 putenv('TTRSS_ICONS_URL=feed-icons');
37 putenv('TTRSS_SELF_URL_PATH=${cfg.selfUrlPath}');
39 putenv('TTRSS_MYSQL_CHARSET=UTF8');
41 putenv('TTRSS_DB_TYPE=${cfg.database.type}');
42 putenv('TTRSS_DB_HOST=${optionalString (cfg.database.host != null) cfg.database.host}');
43 putenv('TTRSS_DB_USER=${cfg.database.user}');
44 putenv('TTRSS_DB_NAME=${cfg.database.name}');
45 putenv('TTRSS_DB_PASS=' ${optionalString (password != null) ". ${password}"});
46 putenv('TTRSS_DB_PORT=${toString dbPort}');
48 putenv('TTRSS_AUTH_AUTO_CREATE=${boolToString cfg.auth.autoCreate}');
49 putenv('TTRSS_AUTH_AUTO_LOGIN=${boolToString cfg.auth.autoLogin}');
51 putenv('TTRSS_FEED_CRYPT_KEY=${escape ["'" "\\"] cfg.feedCryptKey}');
54 putenv('TTRSS_SINGLE_USER_MODE=${boolToString cfg.singleUserMode}');
56 putenv('TTRSS_SIMPLE_UPDATE_MODE=${boolToString cfg.simpleUpdateMode}');
58 # Never check for updates - the running version of the code should
59 # be controlled entirely by the version of TT-RSS active in the
60 # current Nix profile. If TT-RSS updates itself to a version
61 # requiring a database schema upgrade, and then the SystemD
62 # tt-rss.service is restarted, the old code copied from the Nix
63 # store will overwrite the updated version, causing the code to
64 # detect the need for a schema "upgrade" (since the schema version
65 # in the database is different than in the code), but the update
66 # schema operation in TT-RSS will do nothing because the schema
67 # version in the database is newer than that in the code.
68 putenv('TTRSS_CHECK_FOR_UPDATES=false');
70 putenv('TTRSS_FORCE_ARTICLE_PURGE=${toString cfg.forceArticlePurge}');
71 putenv('TTRSS_SESSION_COOKIE_LIFETIME=${toString cfg.sessionCookieLifetime}');
72 putenv('TTRSS_ENABLE_GZIP_OUTPUT=${boolToString cfg.enableGZipOutput}');
74 putenv('TTRSS_PLUGINS=${builtins.concatStringsSep "," cfg.plugins}');
76 putenv('TTRSS_LOG_DESTINATION=${cfg.logDestination}');
77 putenv('TTRSS_CONFIG_VERSION=${toString configVersion}');
80 putenv('TTRSS_PUBSUBHUBBUB_ENABLED=${boolToString cfg.pubSubHubbub.enable}');
81 putenv('TTRSS_PUBSUBHUBBUB_HUB=${cfg.pubSubHubbub.hub}');
83 putenv('TTRSS_SPHINX_SERVER=${cfg.sphinx.server}');
84 putenv('TTRSS_SPHINX_INDEX=${builtins.concatStringsSep "," cfg.sphinx.index}');
86 putenv('TTRSS_ENABLE_REGISTRATION=${boolToString cfg.registration.enable}');
87 putenv('TTRSS_REG_NOTIFY_ADDRESS=${cfg.registration.notifyAddress}');
88 putenv('TTRSS_REG_MAX_USERS=${toString cfg.registration.maxUsers}');
90 putenv('TTRSS_SMTP_SERVER=${cfg.email.server}');
91 putenv('TTRSS_SMTP_LOGIN=${cfg.email.login}');
92 putenv('TTRSS_SMTP_PASSWORD=${escape ["'" "\\"] cfg.email.password}');
93 putenv('TTRSS_SMTP_SECURE=${cfg.email.security}');
95 putenv('TTRSS_SMTP_FROM_NAME=${escape ["'" "\\"] cfg.email.fromName}');
96 putenv('TTRSS_SMTP_FROM_ADDRESS=${escape ["'" "\\"] cfg.email.fromAddress}');
97 putenv('TTRSS_DIGEST_SUBJECT=${escape ["'" "\\"] cfg.email.digestSubject}');
102 # tt-rss and plugins and themes and config.php
103 servedRoot = pkgs.runCommand "tt-rss-served-root" {} ''
104 cp --no-preserve=mode -r ${pkgs.tt-rss} $out
105 cp ${tt-rss-config} $out/config.php
106 ${optionalString (cfg.pluginPackages != []) ''
107 for plugin in ${concatStringsSep " " cfg.pluginPackages}; do
108 cp -r "$plugin"/* "$out/plugins.local/"
111 ${optionalString (cfg.themePackages != []) ''
112 for theme in ${concatStringsSep " " cfg.themePackages}; do
113 cp -r "$theme"/* "$out/themes.local/"
126 enable = mkEnableOption "tt-rss";
130 default = "/var/lib/tt-rss";
132 Root of the application.
140 User account under which both the update daemon and the web-application run.
146 default = "${poolName}";
148 Name of existing phpfpm pool that is used to run web-application.
149 If not specified a pool will be created automatically with
154 virtualHost = mkOption {
155 type = types.nullOr types.str;
158 Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
164 type = types.enum ["pgsql" "mysql"];
167 Database to store feeds. Supported are pgsql and mysql.
172 type = types.nullOr types.str;
175 Host of the database. Leave null to use Unix domain socket.
183 Name of the existing database.
191 The database user. The user must exist and has access to
192 the specified database.
196 password = mkOption {
197 type = types.nullOr types.str;
200 The database user's password.
204 passwordFile = mkOption {
205 type = types.nullOr types.str;
208 The database user's password.
213 type = types.nullOr types.port;
216 The database's port. If not set, the default ports will be provided (5432
217 and 3306 for pgsql and mysql respectively).
221 createLocally = mkOption {
224 description = "Create the database and database user locally.";
229 autoCreate = mkOption {
233 Allow authentication modules to auto-create users in tt-rss internal
234 database when authenticated successfully.
238 autoLogin = mkOption {
242 Automatically login user on remote or other kind of externally supplied
243 authentication, otherwise redirect to login form as normal.
244 If set to true, users won't be able to set application language
245 and settings profile.
255 URL to a PubSubHubbub-compatible hub server. If defined, "Published
256 articles" generated feed would automatically become PUSH-enabled.
264 Enable client PubSubHubbub support in tt-rss. When disabled, tt-rss
265 won't try to subscribe to PUSH feed updates.
273 default = "localhost:9312";
275 Hostname:port combination for the Sphinx server.
280 type = types.listOf types.str;
281 default = ["ttrss" "delta"];
283 Index names in Sphinx configuration. Example configuration
284 files are available on tt-rss wiki.
294 Allow users to register themselves. Please be aware that allowing
295 random people to access your tt-rss installation is a security risk
296 and potentially might lead to data loss or server exploit. Disabled
301 notifyAddress = mkOption {
305 Email address to send new user notifications to.
309 maxUsers = mkOption {
313 Maximum amount of users which will be allowed to register on this
314 system. 0 - no limit.
323 example = "localhost:25";
325 Hostname:port combination to send outgoing mail. Blank - use system
334 SMTP authentication login used when sending outgoing mail.
338 password = mkOption {
342 SMTP authentication password used when sending outgoing mail.
346 security = mkOption {
347 type = types.enum ["" "ssl" "tls"];
350 Used to select a secure SMTP connection. Allowed values: ssl, tls,
355 fromName = mkOption {
357 default = "Tiny Tiny RSS";
359 Name for sending outgoing mail. This applies to password reset
360 notifications, digest emails and any other mail.
364 fromAddress = mkOption {
368 Address for sending outgoing mail. This applies to password reset
369 notifications, digest emails and any other mail.
373 digestSubject = mkOption {
375 default = "[tt-rss] New headlines for last 24 hours";
377 Subject line for email digests.
382 sessionCookieLifetime = mkOption {
386 Default lifetime of a session (e.g. login) cookie. In seconds,
387 0 means cookie will be deleted when browser closes.
391 selfUrlPath = mkOption {
394 Full URL of your tt-rss installation. This should be set to the
395 location of tt-rss directory, e.g. http://example.org/tt-rss/
396 You need to set this option correctly otherwise several features
397 including PUSH, bookmarklets and browser integration will not work properly.
399 example = "http://localhost";
402 feedCryptKey = mkOption {
406 Key used for encryption of passwords for password-protected feeds
407 in the database. A string of 24 random characters. If left blank, encryption
408 is not used. Requires mcrypt functions.
409 Warning: changing this key will make your stored feed passwords impossible
414 singleUserMode = mkOption {
419 Operate in single user mode, disables all functionality related to
420 multiple users and authentication. Enabling this assumes you have
421 your tt-rss directory protected by other means (e.g. http auth).
425 simpleUpdateMode = mkOption {
429 Enables fallback update mode where tt-rss tries to update feeds in
430 background while tt-rss is open in your browser.
431 If you don't have a lot of feeds and don't want to or can't run
432 background processes while not running tt-rss, this method is generally
433 viable to keep your feeds up to date.
434 Still, there are more robust (and recommended) updating methods
435 available, you can read about them here: <https://tt-rss.org/wiki/UpdatingFeeds>
439 forceArticlePurge = mkOption {
443 When this option is not 0, users ability to control feed purging
444 intervals is disabled and all articles (which are not starred)
445 older than this amount of days are purged.
449 enableGZipOutput = mkOption {
453 Selectively gzip output to improve wire performance. This requires
454 PHP Zlib extension on the server.
455 Enabling this can break tt-rss in several httpd/php configurations,
456 if you experience weird errors and tt-rss failing to start, blank pages
457 after login, or content encoding errors, disable it.
461 phpPackage = lib.mkOption {
462 type = lib.types.package;
464 defaultText = "pkgs.php";
466 php package to use for php fpm and update daemon.
471 type = types.listOf types.str;
472 default = ["auth_internal" "note"];
474 List of plugins to load automatically for all users.
475 System plugins have to be specified here. Please enable at least one
476 authentication plugin here (auth_*).
477 Users may enable other user plugins from Preferences/Plugins but may not
478 disable plugins specified in this list.
479 Disabling auth_internal in this list would automatically disable
480 reset password link on the login form.
484 pluginPackages = mkOption {
485 type = types.listOf types.package;
488 List of plugins to install. The list elements are expected to
489 be derivations. All elements in this derivation are automatically
490 copied to the `plugins.local` directory.
494 themePackages = mkOption {
495 type = types.listOf types.package;
498 List of themes to install. The list elements are expected to
499 be derivations. All elements in this derivation are automatically
500 copied to the `themes.local` directory.
504 logDestination = mkOption {
505 type = types.enum ["" "sql" "syslog"];
508 Log destination to use. Possible values: sql (uses internal logging
509 you can read in Preferences -> System), syslog - logs to system log.
510 Setting this to blank uses PHP logging (usually to http server
515 extraConfig = mkOption {
519 Additional lines to append to `config.php`.
526 (mkRemovedOptionModule ["services" "tt-rss" "checkForUpdates"] ''
527 This option was removed because setting this to true will cause TT-RSS
528 to be unable to start if an automatic update of the code in
529 services.tt-rss.root leads to a database schema upgrade that is not
530 supported by the code active in the Nix store.
534 ###### implementation
536 config = mkIf cfg.enable {
540 assertion = cfg.database.password != null -> cfg.database.passwordFile == null;
541 message = "Cannot set both password and passwordFile";
544 assertion = cfg.database.createLocally -> cfg.database.name == cfg.user && cfg.database.user == cfg.user;
546 When creating a database via NixOS, the db user and db name must be equal!
547 If you already have an existing DB+user and this assertion is new, you can safely set
548 `services.tt-rss.database.createLocally` to `false` because removal of `ensureUsers`
549 and `ensureDatabases` doesn't have any effect.
554 services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
558 settings = mapAttrs (name: mkDefault) {
559 "listen.owner" = "nginx";
560 "listen.group" = "nginx";
561 "listen.mode" = "0600";
563 "pm.max_children" = 75;
564 "pm.start_servers" = 10;
565 "pm.min_spare_servers" = 5;
566 "pm.max_spare_servers" = 20;
567 "pm.max_requests" = 500;
568 "catch_workers_output" = 1;
573 # NOTE: No configuration is done if not using virtual host
574 services.nginx = mkIf (cfg.virtualHost != null) {
577 ${cfg.virtualHost} = {
578 root = "${cfg.root}/www";
584 locations."^~ /feed-icons" = {
585 root = "${cfg.root}";
588 locations."~ \\.php$" = {
590 fastcgi_split_path_info ^(.+\.php)(/.+)$;
591 fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
592 fastcgi_index index.php;
599 systemd.tmpfiles.rules = [
600 "d '${cfg.root}' 0555 ${cfg.user} tt_rss - -"
601 "d '${cfg.root}/lock' 0755 ${cfg.user} tt_rss - -"
602 "d '${cfg.root}/cache' 0755 ${cfg.user} tt_rss - -"
603 "d '${cfg.root}/cache/upload' 0755 ${cfg.user} tt_rss - -"
604 "d '${cfg.root}/cache/images' 0755 ${cfg.user} tt_rss - -"
605 "d '${cfg.root}/cache/export' 0755 ${cfg.user} tt_rss - -"
606 "d '${cfg.root}/feed-icons' 0755 ${cfg.user} tt_rss - -"
607 "L+ '${cfg.root}/www' - - - - ${servedRoot}"
611 phpfpm-tt-rss = mkIf (cfg.pool == "${poolName}") {
612 restartTriggers = [ servedRoot ];
616 description = "Tiny Tiny RSS feeds update daemon";
619 ${phpPackage}/bin/php ${cfg.root}/www/update.php --update-schema --force-yes
623 User = "${cfg.user}";
625 ExecStart = "${phpPackage}/bin/php ${cfg.root}/www/update.php --daemon --quiet";
626 Restart = "on-failure";
628 SyslogIdentifier = "tt-rss";
631 wantedBy = [ "multi-user.target" ];
632 requires = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
633 after = [ "network.target" ] ++ optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
637 services.mysql = mkIf mysqlLocal {
639 package = mkDefault pkgs.mariadb;
640 ensureDatabases = [ cfg.database.name ];
644 ensurePermissions = {
645 "${cfg.database.name}.*" = "ALL PRIVILEGES";
651 services.postgresql = mkIf pgsqlLocal {
652 enable = mkDefault true;
653 ensureDatabases = [ cfg.database.name ];
655 { name = cfg.database.user;
656 ensureDBOwnership = true;
661 users.users.tt_rss = optionalAttrs (cfg.user == "tt_rss") {
662 description = "tt-rss service user";
667 users.groups.tt_rss = {};