grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / misc / forgejo.nix
blob9aa7b13b02e191f0d07077d90c1ec4148a6589f4
1 { config, lib, options, pkgs, ... }:
3 let
4   cfg = config.services.forgejo;
5   opt = options.services.forgejo;
6   format = pkgs.formats.ini { };
8   exe = lib.getExe cfg.package;
10   pg = config.services.postgresql;
11   useMysql = cfg.database.type == "mysql";
12   usePostgresql = cfg.database.type == "postgres";
13   useSqlite = cfg.database.type == "sqlite3";
15   secrets = let
16     mkSecret = section: values: lib.mapAttrsToList (key: value: {
17       env = envEscape "FORGEJO__${section}__${key}__FILE";
18       path = value;
19     }) values;
20     # https://codeberg.org/forgejo/forgejo/src/tag/v7.0.2/contrib/environment-to-ini/environment-to-ini.go
21     envEscape = string: lib.replaceStrings [ "." "-" ] [ "_0X2E_" "_0X2D_" ] (lib.strings.toUpper string);
22   in lib.flatten (lib.mapAttrsToList mkSecret cfg.secrets);
24   inherit (lib)
25     literalExpression
26     mkChangedOptionModule
27     mkDefault
28     mkEnableOption
29     mkIf
30     mkMerge
31     mkOption
32     mkPackageOption
33     mkRemovedOptionModule
34     mkRenamedOptionModule
35     optionalAttrs
36     optionals
37     optionalString
38     types
39     ;
42   imports = [
43     (mkRenamedOptionModule [ "services" "forgejo" "appName" ] [ "services" "forgejo" "settings" "DEFAULT" "APP_NAME" ])
44     (mkRemovedOptionModule [ "services" "forgejo" "extraConfig" ] "services.forgejo.extraConfig has been removed. Please use the freeform services.forgejo.settings option instead")
45     (mkRemovedOptionModule [ "services" "forgejo" "database" "password" ] "services.forgejo.database.password has been removed. Please use services.forgejo.database.passwordFile instead")
46     (mkRenamedOptionModule [ "services" "forgejo" "mailerPasswordFile" ] [ "services" "forgejo" "secrets" "mailer" "PASSWD" ])
48     # copied from services.gitea; remove at some point
49     (mkRenamedOptionModule [ "services" "forgejo" "cookieSecure" ] [ "services" "forgejo" "settings" "session" "COOKIE_SECURE" ])
50     (mkRenamedOptionModule [ "services" "forgejo" "disableRegistration" ] [ "services" "forgejo" "settings" "service" "DISABLE_REGISTRATION" ])
51     (mkRenamedOptionModule [ "services" "forgejo" "domain" ] [ "services" "forgejo" "settings" "server" "DOMAIN" ])
52     (mkRenamedOptionModule [ "services" "forgejo" "httpAddress" ] [ "services" "forgejo" "settings" "server" "HTTP_ADDR" ])
53     (mkRenamedOptionModule [ "services" "forgejo" "httpPort" ] [ "services" "forgejo" "settings" "server" "HTTP_PORT" ])
54     (mkRenamedOptionModule [ "services" "forgejo" "log" "level" ] [ "services" "forgejo" "settings" "log" "LEVEL" ])
55     (mkRenamedOptionModule [ "services" "forgejo" "log" "rootPath" ] [ "services" "forgejo" "settings" "log" "ROOT_PATH" ])
56     (mkRenamedOptionModule [ "services" "forgejo" "rootUrl" ] [ "services" "forgejo" "settings" "server" "ROOT_URL" ])
57     (mkRenamedOptionModule [ "services" "forgejo" "ssh" "clonePort" ] [ "services" "forgejo" "settings" "server" "SSH_PORT" ])
58     (mkRenamedOptionModule [ "services" "forgejo" "staticRootPath" ] [ "services" "forgejo" "settings" "server" "STATIC_ROOT_PATH" ])
59     (mkChangedOptionModule [ "services" "forgejo" "enableUnixSocket" ] [ "services" "forgejo" "settings" "server" "PROTOCOL" ] (
60       config: if config.services.forgejo.enableUnixSocket then "http+unix" else "http"
61     ))
62     (mkRemovedOptionModule [ "services" "forgejo" "ssh" "enable" ] "services.forgejo.ssh.enable has been migrated into freeform setting services.forgejo.settings.server.DISABLE_SSH. Keep in mind that the setting is inverted")
63   ];
65   options = {
66     services.forgejo = {
67       enable = mkEnableOption "Forgejo, a software forge";
69       package = mkPackageOption pkgs "forgejo-lts" { };
71       useWizard = mkOption {
72         default = false;
73         type = types.bool;
74         description = ''
75           Whether to use the built-in installation wizard instead of
76           declaratively managing the {file}`app.ini` config file in nix.
77         '';
78       };
80       stateDir = mkOption {
81         default = "/var/lib/forgejo";
82         type = types.str;
83         description = "Forgejo data directory.";
84       };
86       customDir = mkOption {
87         default = "${cfg.stateDir}/custom";
88         defaultText = literalExpression ''"''${config.${opt.stateDir}}/custom"'';
89         type = types.str;
90         description = ''
91           Base directory for custom templates and other options.
93           If {option}`${opt.useWizard}` is disabled (default), this directory will also
94           hold secrets and the resulting {file}`app.ini` config at runtime.
95         '';
96       };
98       user = mkOption {
99         type = types.str;
100         default = "forgejo";
101         description = "User account under which Forgejo runs.";
102       };
104       group = mkOption {
105         type = types.str;
106         default = "forgejo";
107         description = "Group under which Forgejo runs.";
108       };
110       database = {
111         type = mkOption {
112           type = types.enum [ "sqlite3" "mysql" "postgres" ];
113           example = "mysql";
114           default = "sqlite3";
115           description = "Database engine to use.";
116         };
118         host = mkOption {
119           type = types.str;
120           default = "127.0.0.1";
121           description = "Database host address.";
122         };
124         port = mkOption {
125           type = types.port;
126           default = if usePostgresql then pg.settings.port else 3306;
127           defaultText = literalExpression ''
128             if config.${opt.database.type} != "postgresql"
129             then 3306
130             else 5432
131           '';
132           description = "Database host port.";
133         };
135         name = mkOption {
136           type = types.str;
137           default = "forgejo";
138           description = "Database name.";
139         };
141         user = mkOption {
142           type = types.str;
143           default = "forgejo";
144           description = "Database user.";
145         };
147         passwordFile = mkOption {
148           type = types.nullOr types.path;
149           default = null;
150           example = "/run/keys/forgejo-dbpassword";
151           description = ''
152             A file containing the password corresponding to
153             {option}`${opt.database.user}`.
154           '';
155         };
157         socket = mkOption {
158           type = types.nullOr types.path;
159           default = if (cfg.database.createDatabase && usePostgresql) then "/run/postgresql" else if (cfg.database.createDatabase && useMysql) then "/run/mysqld/mysqld.sock" else null;
160           defaultText = literalExpression "null";
161           example = "/run/mysqld/mysqld.sock";
162           description = "Path to the unix socket file to use for authentication.";
163         };
165         path = mkOption {
166           type = types.str;
167           default = "${cfg.stateDir}/data/forgejo.db";
168           defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/forgejo.db"'';
169           description = "Path to the sqlite3 database file.";
170         };
172         createDatabase = mkOption {
173           type = types.bool;
174           default = true;
175           description = "Whether to create a local database automatically.";
176         };
177       };
179       dump = {
180         enable = mkEnableOption "periodic dumps via the [built-in {command}`dump` command](https://forgejo.org/docs/latest/admin/command-line/#dump)";
182         interval = mkOption {
183           type = types.str;
184           default = "04:31";
185           example = "hourly";
186           description = ''
187             Run a Forgejo dump at this interval. Runs by default at 04:31 every day.
189             The format is described in
190             {manpage}`systemd.time(7)`.
191           '';
192         };
194         backupDir = mkOption {
195           type = types.str;
196           default = "${cfg.stateDir}/dump";
197           defaultText = literalExpression ''"''${config.${opt.stateDir}}/dump"'';
198           description = "Path to the directory where the dump archives will be stored.";
199         };
201         type = mkOption {
202           type = types.enum [ "zip" "tar" "tar.sz" "tar.gz" "tar.xz" "tar.bz2" "tar.br" "tar.lz4" "tar.zst" ];
203           default = "zip";
204           description = "Archive format used to store the dump file.";
205         };
207         file = mkOption {
208           type = types.nullOr types.str;
209           default = null;
210           description = "Filename to be used for the dump. If `null` a default name is chosen by forgejo.";
211           example = "forgejo-dump";
212         };
213       };
215       lfs = {
216         enable = mkOption {
217           type = types.bool;
218           default = false;
219           description = "Enables git-lfs support.";
220         };
222         contentDir = mkOption {
223           type = types.str;
224           default = "${cfg.stateDir}/data/lfs";
225           defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/lfs"'';
226           description = "Where to store LFS files.";
227         };
228       };
230       repositoryRoot = mkOption {
231         type = types.str;
232         default = "${cfg.stateDir}/repositories";
233         defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"'';
234         description = "Path to the git repositories.";
235       };
237       settings = mkOption {
238         default = { };
239         description = ''
240           Free-form settings written directly to the `app.ini` configfile file.
241           Refer to <https://forgejo.org/docs/latest/admin/config-cheat-sheet/> for supported values.
242         '';
243         example = literalExpression ''
244           {
245             DEFAULT = {
246               RUN_MODE = "dev";
247             };
248             "cron.sync_external_users" = {
249               RUN_AT_START = true;
250               SCHEDULE = "@every 24h";
251               UPDATE_EXISTING = true;
252             };
253             mailer = {
254               ENABLED = true;
255               MAILER_TYPE = "sendmail";
256               FROM = "do-not-reply@example.org";
257               SENDMAIL_PATH = "''${pkgs.system-sendmail}/bin/sendmail";
258             };
259             other = {
260               SHOW_FOOTER_VERSION = false;
261             };
262           }
263         '';
264         type = types.submodule {
265           freeformType = format.type;
266           options = {
267             log = {
268               ROOT_PATH = mkOption {
269                 default = "${cfg.stateDir}/log";
270                 defaultText = literalExpression ''"''${config.${opt.stateDir}}/log"'';
271                 type = types.str;
272                 description = "Root path for log files.";
273               };
274               LEVEL = mkOption {
275                 default = "Info";
276                 type = types.enum [ "Trace" "Debug" "Info" "Warn" "Error" "Critical" ];
277                 description = "General log level.";
278               };
279             };
281             server = {
282               PROTOCOL = mkOption {
283                 type = types.enum [ "http" "https" "fcgi" "http+unix" "fcgi+unix" ];
284                 default = "http";
285                 description = ''Listen protocol. `+unix` means "over unix", not "in addition to."'';
286               };
288               HTTP_ADDR = mkOption {
289                 type = types.either types.str types.path;
290                 default = if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/forgejo/forgejo.sock" else "0.0.0.0";
291                 defaultText = literalExpression ''if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/forgejo/forgejo.sock" else "0.0.0.0"'';
292                 description = "Listen address. Must be a path when using a unix socket.";
293               };
295               HTTP_PORT = mkOption {
296                 type = types.port;
297                 default = 3000;
298                 description = "Listen port. Ignored when using a unix socket.";
299               };
301               DOMAIN = mkOption {
302                 type = types.str;
303                 default = "localhost";
304                 description = "Domain name of your server.";
305               };
307               ROOT_URL = mkOption {
308                 type = types.str;
309                 default = "http://${cfg.settings.server.DOMAIN}:${toString cfg.settings.server.HTTP_PORT}/";
310                 defaultText = literalExpression ''"http://''${config.services.forgejo.settings.server.DOMAIN}:''${toString config.services.forgejo.settings.server.HTTP_PORT}/"'';
311                 description = "Full public URL of Forgejo server.";
312               };
314               STATIC_ROOT_PATH = mkOption {
315                 type = types.either types.str types.path;
316                 default = cfg.package.data;
317                 defaultText = literalExpression "config.${opt.package}.data";
318                 example = "/var/lib/forgejo/data";
319                 description = "Upper level of template and static files path.";
320               };
322               DISABLE_SSH = mkOption {
323                 type = types.bool;
324                 default = false;
325                 description = "Disable external SSH feature.";
326               };
328               SSH_PORT = mkOption {
329                 type = types.port;
330                 default = 22;
331                 example = 2222;
332                 description = ''
333                   SSH port displayed in clone URL.
334                   The option is required to configure a service when the external visible port
335                   differs from the local listening port i.e. if port forwarding is used.
336                 '';
337               };
338             };
340             session = {
341               COOKIE_SECURE = mkOption {
342                 type = types.bool;
343                 default = false;
344                 description = ''
345                   Marks session cookies as "secure" as a hint for browsers to only send
346                   them via HTTPS. This option is recommend, if Forgejo is being served over HTTPS.
347                 '';
348               };
349             };
350           };
351         };
352       };
354       secrets = mkOption {
355         default = { };
356         description = ''
357           This is a small wrapper over systemd's `LoadCredential`.
359           It takes the same sections and keys as {option}`services.forgejo.settings`,
360           but the value of each key is a path instead of a string or bool.
362           The path is then loaded as credential, exported as environment variable
363           and then feed through
364           <https://codeberg.org/forgejo/forgejo/src/branch/forgejo/contrib/environment-to-ini/environment-to-ini.go>.
366           It does the required environment variable escaping for you.
368           ::: {.note}
369           Keys specified here take priority over the ones in {option}`services.forgejo.settings`!
370           :::
371         '';
372         example = literalExpression ''
373         {
374           metrics = {
375             TOKEN = "/run/keys/forgejo-metrics-token";
376           };
377           camo = {
378             HMAC_KEY = "/run/keys/forgejo-camo-hmac";
379           };
380           service = {
381             HCAPTCHA_SECRET = "/run/keys/forgejo-hcaptcha-secret";
382             HCAPTCHA_SITEKEY = "/run/keys/forgejo-hcaptcha-sitekey";
383           };
384         }
385         '';
386         type = types.submodule {
387           freeformType = with types; attrsOf (attrsOf path);
388           options = { };
389         };
390       };
391     };
392   };
394   config = mkIf cfg.enable {
395     assertions = [
396       {
397         assertion = cfg.database.createDatabase -> useSqlite || cfg.database.user == cfg.user;
398         message = "services.forgejo.database.user must match services.forgejo.user if the database is to be automatically provisioned";
399       }
400       { assertion = cfg.database.createDatabase && usePostgresql -> cfg.database.user == cfg.database.name;
401         message = ''
402           When creating a database via NixOS, the db user and db name must be equal!
403           If you already have an existing DB+user and this assertion is new, you can safely set
404           `services.forgejo.createDatabase` to `false` because removal of `ensureUsers`
405           and `ensureDatabases` doesn't have any effect.
406         '';
407       }
408     ];
410     services.forgejo.settings = {
411       DEFAULT = {
412         RUN_MODE = mkDefault "prod";
413         RUN_USER = mkDefault cfg.user;
414         WORK_PATH = mkDefault cfg.stateDir;
415       };
417       database = mkMerge [
418         {
419           DB_TYPE = cfg.database.type;
420         }
421         (mkIf (useMysql || usePostgresql) {
422           HOST = if cfg.database.socket != null then cfg.database.socket else cfg.database.host + ":" + toString cfg.database.port;
423           NAME = cfg.database.name;
424           USER = cfg.database.user;
425         })
426         (mkIf useSqlite {
427           PATH = cfg.database.path;
428         })
429         (mkIf usePostgresql {
430           SSL_MODE = "disable";
431         })
432       ];
434       repository = {
435         ROOT = cfg.repositoryRoot;
436       };
438       server = mkIf cfg.lfs.enable {
439         LFS_START_SERVER = true;
440       };
442       session = {
443         COOKIE_NAME = mkDefault "session";
444       };
446       security = {
447         INSTALL_LOCK = true;
448       };
450       lfs = mkIf cfg.lfs.enable {
451         PATH = cfg.lfs.contentDir;
452       };
453     };
455     services.forgejo.secrets = {
456       security = {
457         SECRET_KEY = "${cfg.customDir}/conf/secret_key";
458         INTERNAL_TOKEN = "${cfg.customDir}/conf/internal_token";
459       };
461       oauth2 = {
462         JWT_SECRET = "${cfg.customDir}/conf/oauth2_jwt_secret";
463       };
465       database = mkIf (cfg.database.passwordFile != null) {
466         PASSWD = cfg.database.passwordFile;
467       };
469       server = mkIf cfg.lfs.enable {
470         LFS_JWT_SECRET = "${cfg.customDir}/conf/lfs_jwt_secret";
471       };
472     };
474     services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) {
475       enable = mkDefault true;
477       ensureDatabases = [ cfg.database.name ];
478       ensureUsers = [
479         {
480           name = cfg.database.user;
481           ensureDBOwnership = true;
482         }
483       ];
484     };
486     services.mysql = optionalAttrs (useMysql && cfg.database.createDatabase) {
487       enable = mkDefault true;
488       package = mkDefault pkgs.mariadb;
490       ensureDatabases = [ cfg.database.name ];
491       ensureUsers = [
492         {
493           name = cfg.database.user;
494           ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
495         }
496       ];
497     };
499     systemd.tmpfiles.rules = [
500       "d '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -"
501       "z '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -"
502       "d '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -"
503       "z '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -"
504       "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
505       "d '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
506       "d '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -"
507       "d '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
508       "d '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -"
509       "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
510       "z '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
511       "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} ${cfg.group} - -"
512       "z '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
513       "z '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -"
514       "z '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
515       "z '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -"
516       "z '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
518       # If we have a folder or symlink with Forgejo locales, remove it
519       # And symlink the current Forgejo locales in place
520       "L+ '${cfg.stateDir}/conf/locale' - - - - ${cfg.package.out}/locale"
522     ] ++ optionals cfg.lfs.enable [
523       "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -"
524       "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -"
525     ];
527     systemd.services.forgejo-secrets = mkIf (!cfg.useWizard) {
528       description = "Forgejo secret bootstrap helper";
529       script = ''
530         if [ ! -s '${cfg.secrets.security.SECRET_KEY}' ]; then
531             ${exe} generate secret SECRET_KEY > '${cfg.secrets.security.SECRET_KEY}'
532         fi
534         if [ ! -s '${cfg.secrets.oauth2.JWT_SECRET}' ]; then
535             ${exe} generate secret JWT_SECRET > '${cfg.secrets.oauth2.JWT_SECRET}'
536         fi
538         ${optionalString cfg.lfs.enable ''
539         if [ ! -s '${cfg.secrets.server.LFS_JWT_SECRET}' ]; then
540             ${exe} generate secret LFS_JWT_SECRET > '${cfg.secrets.server.LFS_JWT_SECRET}'
541         fi
542         ''}
544         if [ ! -s '${cfg.secrets.security.INTERNAL_TOKEN}' ]; then
545             ${exe} generate secret INTERNAL_TOKEN > '${cfg.secrets.security.INTERNAL_TOKEN}'
546         fi
547       '';
548       serviceConfig = {
549         Type = "oneshot";
550         RemainAfterExit = true;
551         User = cfg.user;
552         Group = cfg.group;
553         ReadWritePaths = [ cfg.customDir ];
554         UMask = "0077";
555       };
556     };
558     systemd.services.forgejo = {
559       description = "Forgejo (Beyond coding. We forge.)";
560       after = [
561         "network.target"
562       ] ++ optionals usePostgresql [
563         "postgresql.service"
564       ] ++ optionals useMysql [
565         "mysql.service"
566       ] ++ optionals (!cfg.useWizard) [
567         "forgejo-secrets.service"
568       ];
569       requires = optionals (cfg.database.createDatabase && usePostgresql) [
570         "postgresql.service"
571       ] ++ optionals (cfg.database.createDatabase && useMysql) [
572         "mysql.service"
573       ] ++ optionals (!cfg.useWizard) [
574         "forgejo-secrets.service"
575       ];
576       wantedBy = [ "multi-user.target" ];
577       path = [ cfg.package pkgs.git pkgs.gnupg ];
579       # In older versions the secret naming for JWT was kind of confusing.
580       # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET
581       # wasn't persistent at all.
582       # To fix that, there is now the file oauth2_jwt_secret containing the
583       # values for JWT_SECRET and the file jwt_secret gets renamed to
584       # lfs_jwt_secret.
585       # We have to consider this to stay compatible with older installations.
586       preStart =
587         ''
588           ${optionalString (!cfg.useWizard) ''
589             function forgejo_setup {
590               config='${cfg.customDir}/conf/app.ini'
591               cp -f '${format.generate "app.ini" cfg.settings}' "$config"
593               chmod u+w "$config"
594               ${lib.getExe' cfg.package "environment-to-ini"} --config "$config"
595               chmod u-w "$config"
596             }
597             (umask 027; forgejo_setup)
598           ''}
600           # run migrations/init the database
601           ${exe} migrate
603           # update all hooks' binary paths
604           ${exe} admin regenerate hooks
606           # update command option in authorized_keys
607           if [ -r ${cfg.stateDir}/.ssh/authorized_keys ]
608           then
609             ${exe} admin regenerate keys
610           fi
611         '';
613       serviceConfig = {
614         Type = "simple";
615         User = cfg.user;
616         Group = cfg.group;
617         WorkingDirectory = cfg.stateDir;
618         ExecStart = "${exe} web --pid /run/forgejo/forgejo.pid";
619         Restart = "always";
620         # Runtime directory and mode
621         RuntimeDirectory = "forgejo";
622         RuntimeDirectoryMode = "0755";
623         # Proc filesystem
624         ProcSubset = "pid";
625         ProtectProc = "invisible";
626         # Access write directories
627         ReadWritePaths = [ cfg.customDir cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ];
628         UMask = "0027";
629         # Capabilities
630         CapabilityBoundingSet = "";
631         # Security
632         NoNewPrivileges = true;
633         # Sandboxing
634         ProtectSystem = "strict";
635         ProtectHome = true;
636         PrivateTmp = true;
637         PrivateDevices = true;
638         PrivateUsers = true;
639         ProtectHostname = true;
640         ProtectClock = true;
641         ProtectKernelTunables = true;
642         ProtectKernelModules = true;
643         ProtectKernelLogs = true;
644         ProtectControlGroups = true;
645         RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
646         RestrictNamespaces = true;
647         LockPersonality = true;
648         MemoryDenyWriteExecute = true;
649         RestrictRealtime = true;
650         RestrictSUIDSGID = true;
651         RemoveIPC = true;
652         PrivateMounts = true;
653         # System Call Filtering
654         SystemCallArchitectures = "native";
655         SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid" "setrlimit" ];
656         # cfg.secrets
657         LoadCredential = map (e: "${e.env}:${e.path}") secrets;
658       };
660       environment = {
661         USER = cfg.user;
662         HOME = cfg.stateDir;
663         # `GITEA_` prefix until https://codeberg.org/forgejo/forgejo/issues/497
664         # is resolved.
665         GITEA_WORK_DIR = cfg.stateDir;
666         GITEA_CUSTOM = cfg.customDir;
667       } // lib.listToAttrs (map (e: lib.nameValuePair e.env "%d/${e.env}") secrets);
668     };
670     services.openssh.settings.AcceptEnv = mkIf (!cfg.settings.START_SSH_SERVER or false) "GIT_PROTOCOL";
672     users.users = mkIf (cfg.user == "forgejo") {
673       forgejo = {
674         home = cfg.stateDir;
675         useDefaultShell = true;
676         group = cfg.group;
677         isSystemUser = true;
678       };
679     };
681     users.groups = mkIf (cfg.group == "forgejo") {
682       forgejo = { };
683     };
685     systemd.services.forgejo-dump = mkIf cfg.dump.enable {
686       description = "forgejo dump";
687       after = [ "forgejo.service" ];
688       path = [ cfg.package ];
690       environment = {
691         USER = cfg.user;
692         HOME = cfg.stateDir;
693         # `GITEA_` prefix until https://codeberg.org/forgejo/forgejo/issues/497
694         # is resolved.
695         GITEA_WORK_DIR = cfg.stateDir;
696         GITEA_CUSTOM = cfg.customDir;
697       };
699       serviceConfig = {
700         Type = "oneshot";
701         User = cfg.user;
702         ExecStart = "${exe} dump --type ${cfg.dump.type}" + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}";
703         WorkingDirectory = cfg.dump.backupDir;
704       };
705     };
707     systemd.timers.forgejo-dump = mkIf cfg.dump.enable {
708       description = "Forgejo dump timer";
709       partOf = [ "forgejo-dump.service" ];
710       wantedBy = [ "timers.target" ];
711       timerConfig.OnCalendar = cfg.dump.interval;
712     };
713   };
715   meta.doc = ./forgejo.md;
716   meta.maintainers = with lib.maintainers; [ bendlas emilylange ];