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