1 { pkgs, config, lib, ... }:
4 inherit (lib) optionalString mkDefault mkIf mkOption mkEnableOption literalExpression;
5 inherit (lib.types) nullOr attrsOf oneOf str int bool path package enum submodule;
6 inherit (lib.strings) concatLines removePrefix toShellVars removeSuffix hasSuffix;
7 inherit (lib.attrsets) mapAttrsToList attrValues genAttrs filterAttrs mapAttrs' nameValuePair;
8 inherit (builtins) isInt isString toString typeOf;
10 cfg = config.services.firefly-iii;
15 defaultUser = "firefly-iii";
16 defaultGroup = "firefly-iii";
18 artisan = "${cfg.package}/artisan";
20 env-file-values = mapAttrs' (n: v: nameValuePair (removeSuffix "_FILE" n) v)
21 (filterAttrs (n: v: hasSuffix "_FILE" n) cfg.settings);
22 env-nonfile-values = filterAttrs (n: v: ! hasSuffix "_FILE" n) cfg.settings;
26 ${toShellVars env-nonfile-values}
27 ${concatLines (mapAttrsToList (n: v: "${n}=\"$(< ${v})\"") env-file-values)}
31 firefly-iii-maintenance = pkgs.writeShellScript "firefly-iii-maintenance.sh" ''
34 ${optionalString (cfg.settings.DB_CONNECTION == "sqlite")
35 "touch ${cfg.dataDir}/storage/database/database.sqlite"}
36 ${artisan} cache:clear
37 ${artisan} package:discover
38 ${artisan} firefly-iii:upgrade-database
39 ${artisan} firefly-iii:laravel-passport-keys
41 ${artisan} route:cache
42 ${artisan} config:cache
45 commonServiceConfig = {
49 StateDirectory = "firefly-iii";
50 ReadWritePaths = [cfg.dataDir];
51 WorkingDirectory = cfg.package;
53 PrivateDevices = true;
54 CapabilityBoundingSet = "";
55 AmbientCapabilities = "";
56 ProtectSystem = "strict";
57 ProtectKernelTunables = true;
58 ProtectKernelModules = true;
59 ProtectControlGroups = true;
61 ProtectHostname = true;
62 ProtectHome = "tmpfs";
63 ProtectKernelLogs = true;
64 ProtectProc = "invisible";
66 PrivateNetwork = false;
67 RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
68 SystemCallArchitectures = "native";
70 "@system-service @resources"
71 "~@obsolete @privileged"
73 RestrictSUIDSGID = true;
75 NoNewPrivileges = true;
76 RestrictRealtime = true;
77 RestrictNamespaces = true;
78 LockPersonality = true;
84 options.services.firefly-iii = {
86 enable = mkEnableOption "Firefly III: A free and open source personal finance manager";
90 default = defaultUser;
91 description = "User account under which firefly-iii runs.";
96 default = if cfg.enableNginx then "nginx" else defaultGroup;
97 defaultText = "If `services.firefly-iii.enableNginx` is true then `nginx` else ${defaultGroup}";
99 Group under which firefly-iii runs. It is best to set this to the group
100 of whatever webserver is being used as the frontend.
106 default = "/var/lib/firefly-iii";
108 The place where firefly-iii stores its state.
114 default = pkgs.firefly-iii;
115 defaultText = literalExpression "pkgs.firefly-iii";
117 The firefly-iii package served by php-fpm and the webserver of choice.
118 This option can be used to point the webserver to the correct root. It
119 may also be used to set the package to a different version, say a
122 apply = firefly-iii : firefly-iii.override (prev: {
123 dataDir = cfg.dataDir;
127 enableNginx = mkOption {
131 Whether to enable nginx or not. If enabled, an nginx virtual host will
132 be created for access to firefly-iii. If not enabled, then you may use
133 `''${config.services.firefly-iii.package}` as your document root in
134 whichever webserver you wish to setup.
138 virtualHost = mkOption {
140 default = "localhost";
142 The hostname at which you wish firefly-iii to be served. If you have
143 enabled nginx using `services.firefly-iii.enableNginx` then this will
148 poolConfig = mkOption {
149 type = attrsOf (oneOf [ str int bool ]);
152 "pm.max_children" = 32;
153 "pm.start_servers" = 2;
154 "pm.min_spare_servers" = 2;
155 "pm.max_spare_servers" = 4;
156 "pm.max_requests" = 500;
159 Options for the Firefly III PHP pool. See the documentation on <literal>php-fpm.conf</literal>
160 for details on configuration directives.
164 settings = mkOption {
167 Options for firefly-iii configuration. Refer to
168 <https://github.com/firefly-iii/firefly-iii/blob/main/.env.example> for
169 details on supported values. All <option>_FILE values supported by
170 upstream are supported here.
172 APP_URL will be the same as `services.firefly-iii.virtualHost` if the
173 former is unset in `services.firefly-iii.settings`.
175 example = literalExpression ''
177 APP_ENV = "production";
178 APP_KEY_FILE = "/var/secrets/firefly-iii-app-key.txt";
179 SITE_OWNER = "mail@example.com";
180 DB_CONNECTION = "mysql";
183 DB_DATABASE = "firefly";
184 DB_USERNAME = "firefly";
185 DB_PASSWORD_FILE = "/var/secrets/firefly-iii-mysql-password.txt;
189 freeformType = attrsOf (oneOf [str int bool]);
191 DB_CONNECTION = mkOption {
192 type = enum [ "sqlite" "pgsql" "mysql" ];
196 The type of database you wish to use. Can be one of "sqlite",
201 type = enum [ "local" "production" "testing" ];
203 example = "production";
205 The app environment. It is recommended to keep this at "local".
206 Possible values are "local", "production" and "testing"
211 default = if cfg.settings.DB_CONNECTION == "pgsql" then 5432
212 else if cfg.settings.DB_CONNECTION == "mysql" then 3306
215 `null` if DB_CONNECTION is "sqlite", `3306` if "mysql", `5432` if "pgsql"
218 The port your database is listening at. sqlite does not require
219 this value to be filled.
224 default = if cfg.settings.DB_CONNECTION == "pgsql" then "/run/postgresql"
227 "localhost" if DB_CONNECTION is "sqlite" or "mysql", "/run/postgresql" if "pgsql".
230 The machine which hosts your database. This is left at the
231 default value for "mysql" because we use the "DB_SOCKET" option
232 to connect to a unix socket instead. "pgsql" requires that the
233 unix socket location be specified here instead of at "DB_SOCKET".
234 This option does not affect "sqlite".
237 APP_KEY_FILE = mkOption {
240 The path to your appkey. The file should contain a 32 character
241 random app key. This may be set using `echo "base64:$(head -c 32
242 /dev/urandom | base64)" > /path/to/key-file`.
247 default = if cfg.virtualHost == "localhost" then "http://${cfg.virtualHost}"
248 else "https://${cfg.virtualHost}";
250 http(s)://''${config.services.firefly-iii.virtualHost}
253 The APP_URL used by firefly-iii internally. Please make sure this
254 URL matches the external URL of your Firefly III installation. It
255 is used to validate specific requests and to generate URLs in
264 config = mkIf cfg.enable {
266 services.phpfpm.pools.firefly-iii = {
268 phpPackage = cfg.package.phpPackage;
273 "listen.mode" = "0660";
274 "listen.owner" = user;
275 "listen.group" = group;
280 systemd.services.firefly-iii-setup = {
281 after = [ "postgresql.service" "mysql.service" ];
282 requiredBy = [ "phpfpm-firefly-iii.service" ];
283 before = [ "phpfpm-firefly-iii.service" ];
285 ExecStart = firefly-iii-maintenance;
286 RemainAfterExit = true;
287 } // commonServiceConfig;
288 unitConfig.JoinsNamespaceOf = "phpfpm-firefly-iii.service";
289 restartTriggers = [ cfg.package ];
292 systemd.services.firefly-iii-cron = {
293 after = [ "firefly-iii-setup.service" "postgresql.service" "mysql.service" ];
294 wants = [ "firefly-iii-setup.service" ];
295 description = "Daily Firefly III cron job";
297 ExecStart = "${artisan} firefly-iii:cron";
298 } // commonServiceConfig;
301 systemd.timers.firefly-iii-cron = {
302 description = "Trigger Firefly Cron";
304 OnCalendar = "Daily";
305 RandomizedDelaySec = "1800s";
308 wantedBy = [ "timers.target" ];
309 restartTriggers = [ cfg.package ];
312 services.nginx = mkIf cfg.enableNginx {
314 recommendedTlsSettings = mkDefault true;
315 recommendedOptimisation = mkDefault true;
316 recommendedGzipSettings = mkDefault true;
317 virtualHosts.${cfg.virtualHost} = {
318 root = "${cfg.package}/public";
321 tryFiles = "$uri $uri/ /index.php?$query_string";
329 include ${config.services.nginx.package}/conf/fastcgi_params ;
330 fastcgi_param SCRIPT_FILENAME $request_filename;
331 fastcgi_param modHeadersAvailable true; #Avoid sending the security headers twice
332 fastcgi_pass unix:${config.services.phpfpm.pools.firefly-iii.socket};
339 systemd.tmpfiles.settings."10-firefly-iii" = genAttrs [
340 "${cfg.dataDir}/storage"
341 "${cfg.dataDir}/storage/app"
342 "${cfg.dataDir}/storage/database"
343 "${cfg.dataDir}/storage/export"
344 "${cfg.dataDir}/storage/framework"
345 "${cfg.dataDir}/storage/framework/cache"
346 "${cfg.dataDir}/storage/framework/sessions"
347 "${cfg.dataDir}/storage/framework/views"
348 "${cfg.dataDir}/storage/logs"
349 "${cfg.dataDir}/storage/upload"
350 "${cfg.dataDir}/cache"
358 "${cfg.dataDir}".d = {
366 users = mkIf (user == defaultUser) {
368 description = "Firefly-iii service user";
374 groups = mkIf (group == defaultGroup) {
375 ${defaultGroup} = {};