diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix new file mode 100644 index 0000000..7b530c3 --- /dev/null +++ b/modules/apps/arr/default.nix @@ -0,0 +1,303 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.arr; + + networkName = "arrStack"; + proxyNet = config.homelab.apps.traefik.sharedNetworkName; + + appNames = [ "bazarr" "prowlarr" "qbittorrent" "radarr" "sonarr" ]; + inUse = builtins.any (app: cfg.${app}.enable) appNames; + + PGID = toString config.users.groups.media.gid; + UMASK = "002"; +in { + options.homelab.apps.arr = let + mkAppOption = appName: { + enable = lib.mkEnableOption "${appName} using Docker"; + exposePorts = lib.mkOption { + type = lib.types.bool; + description = "Expose ${appName} port"; + default = cfg.exposePorts; + }; + }; + in { + enable = lib.mkEnableOption "Arr Stack using Docker"; + exposePorts = lib.mkOption { + type = lib.types.bool; + description = "Expose all app ports"; + # Only expose ports by default if Traefik is not in use. + default = ! config.homelab.apps.traefik.enable; + }; + + bazarr = mkAppOption "Bazarr"; + prowlarr = mkAppOption "Prowlarr"; + qbittorrent = mkAppOption "qBittorrent"; + radarr = mkAppOption "Radarr"; + sonarr = mkAppOption "Sonarr"; + }; + + config = { + homelab = { + users = lib.mkIf inUse { + apps.enable = true; + media.enable = true; + }; + + # "Master switch": Enable all apps. + apps.arr = lib.mkIf cfg.enable { + bazarr.enable = true; + prowlarr.enable = true; + qbittorrent.enable = true; + radarr.enable = true; + sonarr.enable = true; + }; + + fileSystems.media.video = { + enable = true; + permissions = [ "read" "write" ]; + }; + + virtualisation.containers.enable = lib.mkIf inUse true; + }; + + fileSystems = let + mkFileSystem = device: { + inherit device; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "rsize=1048576" "wsize=1048576" + "hard" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + + hugoBackup = "192.168.0.11:/mnt/BIG/BACKUP"; + in lib.mkIf inUse { + "/srv/bazarr-backup" = lib.mkIf cfg.bazarr.enable (mkFileSystem "${hugoBackup}/BAZARR"); + "/srv/prowlarr-backup" = lib.mkIf cfg.bazarr.enable (mkFileSystem "${hugoBackup}/PROWLARR"); + "/srv/qbittorrent" = lib.mkIf cfg.qbittorrent.enable (mkFileSystem "192.168.0.11:/mnt/SMALL/CONFIG/QBITTORRENT"); + "/srv/radarr-backup" = lib.mkIf cfg.radarr.enable (mkFileSystem "${hugoBackup}/RADARR"); + "/srv/sonarr-backup" = lib.mkIf cfg.sonarr.enable (mkFileSystem "${hugoBackup}/SONARR"); + "/srv/torrent" = mkFileSystem "192.168.0.11:/mnt/SMALL/MEDIA/TORRENT"; + }; + + # Make sure the Docker network exists. + systemd.services."docker-${networkName}-create-network" = lib.mkIf inUse { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "docker-bazarr.service" + "docker-prowlarr.service" + "docker-qbittorrent.service" + "docker-radarr.service" + "docker-sonarr.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then + ${pkgs.docker}/bin/docker network create ${networkName} + fi + ''; + }; + + # Create a user for each app. + users.users = let + mkUser = uid: { + uid = lib.mkForce uid; + isSystemUser = true; + group = config.users.groups.media.name; + home = "/var/empty"; + shell = null; + }; + in { + bazarr = lib.mkIf cfg.bazarr.enable (mkUser 3003); + prowlarr = lib.mkIf cfg.prowlarr.enable (mkUser 3004); + qbittorrent = lib.mkIf cfg.qbittorrent.enable (mkUser 3005) // { + extraGroups = [ + config.users.groups.apps.name + ]; + }; + radarr = lib.mkIf cfg.radarr.enable (mkUser 3006); + sonarr = lib.mkIf cfg.sonarr.enable (mkUser 3007); + }; + + virtualisation.oci-containers.containers = let + videoHostPath = config.homelab.fileSystems.media.video.hostPath; + in { + bazarr = let + port = 6767; + in lib.mkIf cfg.bazarr.enable { + hostname = "bazarr"; + image = "ghcr.io/hotio/bazarr:release-1.5.2"; + autoStart = true; + ports = lib.mkIf cfg.bazarr.exposePorts [ + "${toString port}:${toString port}/tcp" + "${toString port}:${toString port}/udp" + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${proxyNet}" + ]; + environment = { + PUID = toString config.users.users.bazarr.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + WEBUI_PORTS = "${toString port}/tcp,${toString port}/udp"; + }; + volumes = [ + "bazarr-config:/config" + + "/srv/bazarr-backup:/config/backup" + + "${videoHostPath}/Films:/media/movies" + "${videoHostPath}/Series:/media/series" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.bazarr.rule" = "Host(`bazarr.depeuter.dev`)"; + "traefik.http.services.bazarr.loadbalancer.server.port" = toString port; + }; + }; + + prowlarr = let + port = 9696; + in lib.mkIf cfg.prowlarr.enable { + hostname = "prowlarr"; + image = "ghcr.io/hotio/prowlarr:release-2.0.5.5160"; + autoStart = true; + ports = lib.mkIf cfg.prowlarr.exposePorts [ + "${toString port}:${toString port}/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${proxyNet}" + ]; + environment = { + PUID = toString config.users.users.prowlarr.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + }; + volumes = [ + "prowlarr-config:/config" + + "/srv/prowlarr-backup:/config/Backups" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.prowlarr.rule" = "Host(`prowlarr.depeuter.dev`)"; + "traefik.http.services.prowlarr.loadbalancer.server.port" = toString port; + }; + }; + + qbittorrent = let + port = 10095; + in lib.mkIf cfg.qbittorrent.enable { + hostname = "qbittorrent"; + image = "ghcr.io/hotio/qbittorrent:release-5.1.2"; + autoStart = true; + ports = lib.mkIf cfg.qbittorrent.exposePorts [ + "${toString port}:${toString port}/tcp" + "${toString port}:${toString port}/udp" + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${proxyNet}" + ]; + environment = { + PUID = toString config.users.users.qbittorrent.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + WEBUI_PORTS = "${toString port}/tcp,${toString port}/udp"; + }; + volumes = [ + "/srv/qbittorrent:/config" + + "/srv/torrent:/media/cache" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.qbittorrent.rule" = "Host(`qb.depeuter.dev`)"; + "traefik.http.services.qbittorrent.loadbalancer.server.port" = toString port; + }; + }; + + radarr = let + port = 7878; + in lib.mkIf cfg.radarr.enable { + hostname = "radarr"; + image = "ghcr.io/hotio/radarr:testing-5.28.0.10205"; + autoStart = true; + ports = lib.mkIf cfg.radarr.exposePorts [ + "${toString port}:${toString port}/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${proxyNet}" + ]; + environment = { + PUID = toString config.users.users.radarr.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + }; + volumes = [ + "radarr-config:/config" + + "/srv/radarr-backup:/config/Backups" + + "/srv/torrent:/media/cache" + "${videoHostPath}/Films:/media/movies" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.radarr.rule" = "Host(`radarr.depeuter.dev`)"; + "traefik.http.services.radarr.loadbalancer.server.port" = toString port; + }; + }; + + sonarr = let + port = 8989; + in lib.mkIf cfg.sonarr.enable { + hostname = "sonarr"; + image = "ghcr.io/hotio/sonarr:release-4.0.15.2941"; + autoStart = true; + ports = lib.mkIf cfg.sonarr.exposePorts [ + "${toString port}:${toString port}/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${proxyNet}" + ]; + environment = { + PUID = toString config.users.users.sonarr.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + }; + volumes = [ + "sonarr-config:/config" + + "/srv/sonarr-backup:/config/Backups" + + "/srv/torrent:/media/cache" + "${videoHostPath}/Series:/media/series" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.sonarr.rule" = "Host(`sonarr.depeuter.dev`)"; + "traefik.http.services.sonarr.loadbalancer.server.port" = toString port; + }; + }; + }; + }; +} diff --git a/modules/apps/calibre/default.nix b/modules/apps/calibre/default.nix new file mode 100644 index 0000000..bddf5c8 --- /dev/null +++ b/modules/apps/calibre/default.nix @@ -0,0 +1,189 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.calibre; + + PUID = toString config.users.users.calibre.uid; + PGID = toString config.users.groups.media.gid; + + books = "/srv/books"; + calibre-config = "/srv/calibre-config"; + calibre-web-config = "/srv/calibre-web-config"; + + networkName = "calibre"; + proxyNet = config.homelab.apps.traefik.sharedNetworkName; +in { + options.homelab.apps.calibre = { + enable = lib.mkEnableOption "Calibre (Desktop + Web)"; + desktop.enable = lib.mkEnableOption "Calibre Desktop (KasmVNC)"; + web.enable = lib.mkEnableOption "Calibre Web"; + }; + + config = lib.mkMerge [ + { + homelab.apps.calibre = lib.mkIf cfg.enable { + desktop.enable = true; + web.enable = true; + }; + } + + # Common + (lib.mkIf (cfg.desktop.enable || cfg.web.enable) { + homelab = { + users.media.enable = true; + virtualisation.containers.enable = true; + }; + + users.users.calibre = { + uid = lib.mkForce 3010; + isSystemUser = true; + group = config.users.groups.media.name; + home = "/var/empty"; + shell = null; + }; + + fileSystems."${books}" = { + device = "192.168.0.11:/mnt/SMALL/MEDIA/BOOKS"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "rsize=1048576" "wsize=1048576" + "soft" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + + # Make sure the Docker network exists. + systemd.services."docker-${networkName}-create-network" = { + requiredBy = [ + "docker-calibre.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then + ${pkgs.docker}/bin/docker network create ${networkName} + fi + ''; + }; + }) + + # Calibre desktop + (lib.mkIf cfg.desktop.enable { + fileSystems."${calibre-config}" = { + device = "192.168.0.11:/mnt/SMALL/CONFIG/CALIBRE"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "rsize=1048576" "wsize=1048576" + "soft" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + + virtualisation.oci-containers.containers.calibre = let + innerPort = 8080; + in { + hostname = "calibre"; + image = "lscr.io/linuxserver/calibre:v8.10.0-ls354"; + autoStart = true; + ports = [ + # Open ports if you don't use Traefik + "9480:${toString innerPort}" # Calibre desktop GUI + #"9481:8181" # Calibre desktop GUI HTTPS + #"9581:8081" # Calibre webserver gui + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${proxyNet}" + + # syscalls are unkown to Docker + #"--security-opt" "seccomp=unconfined" + ]; + environment = { + inherit PUID PGID; + #UMASK = "022"; + + TZ = config.time.timeZone; + + #PASSWORD = ""; + #CLI_ARGS = ""; + }; + volumes = [ + "${calibre-config}:/config" + + "${books}:/media/books" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.calibre.rule" = "Host(`calibre.depeuter.dev`)"; + "traefik.http.services.calibre.loadbalancer.server.port" = toString innerPort; + }; + }; + }) + + # Calibre Web + (lib.mkIf cfg.web.enable { + fileSystems."${calibre-web-config}" = { + device = "192.168.0.11:/mnt/SMALL/CONFIG/CALIBRE-WEB"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "rsize=1048576" "wsize=1048576" + "soft" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + + virtualisation.oci-containers.containers.calibre-web = let + innerPort = 8083; + in { + hostname = "calibre-web"; + image = "lscr.io/linuxserver/calibre-web:0.6.25-ls346"; + autoStart = true; + ports = [ + # Open ports if you don't use Traefik + "8083:${toString innerPort}" # Web UI + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${proxyNet}" + ]; + environment = { + inherit PUID PGID; + #UMASK = "022"; + + TZ = config.time.timeZone; + + # (x86-64 only) Adds the ability to perform ebook conversion + DOCKER_MODS = "linuxserver/mods:universal-calibre"; + # Allow Google Oauth + #OAUTHLIB_RELAX_TOKEN_SCOPE = "1"; + }; + volumes = [ + "${calibre-web-config}:/config" + + "${books}:/media/books" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.calibre-web.rule" = "Host(`books.depeuter.dev`)"; + "traefik.http.services.calibre-web.loadbalancer.server.port" = toString innerPort; + }; + }; + }) + ]; +} diff --git a/modules/apps/changedetection/default.nix b/modules/apps/changedetection/default.nix new file mode 100644 index 0000000..ee88751 --- /dev/null +++ b/modules/apps/changedetection/default.nix @@ -0,0 +1,28 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.changedetection; +in { + options.homelab.apps.changedetection.enable = lib.mkEnableOption "Changedetection.io"; + + config = lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + virtualisation.oci-containers.containers.changedetection = { + hostname = "changedetection"; + image = "ghcr.io/dgtlmoon/changedetection.io"; + autoStart = true; + ports = [ + "5000:5000/tcp" + ]; + extraOptions = [ + ]; + volumes = [ + "changedetection:/datastore" + ]; + environment = { + LOGGER_LEVEL = "WARNING"; + }; + }; + }; +} diff --git a/modules/apps/default.nix b/modules/apps/default.nix index 8ee2e12..385f915 100644 --- a/modules/apps/default.nix +++ b/modules/apps/default.nix @@ -1,6 +1,18 @@ { imports = [ + ./arr ./bind9 + ./calibre + ./changedetection + ./freshrss + ./gitea + ./homepage + ./jellyfin + ./plex + ./solidtime + ./speedtest ./technitium-dns + ./traefik + ./vaultwarden ]; } diff --git a/modules/apps/freshrss/default.nix b/modules/apps/freshrss/default.nix new file mode 100644 index 0000000..4f4456f --- /dev/null +++ b/modules/apps/freshrss/default.nix @@ -0,0 +1,93 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.freshrss; + + networkName = "freshrss"; +in { + options.homelab.apps.freshrss = { + enable = lib.mkEnableOption "FreshRSS"; + port = lib.mkOption { + type = lib.types.int; + default = 9080; + description = "FreshRSS WebUI port"; + }; + }; + + config = let + inherit (config.homelab.apps.freshrss) port; + in + lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + fileSystems."/srv/freshrss" = { + device = "192.168.0.11:/mnt/SMALL/CONFIG/FRESHRSS"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "async" "soft" "timeo=600" + "retrans=2" + "_netdev" + "nosuid" + "tcp" + ]; + }; + + systemd.services."docker-${networkName}-create-network" = { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "docker-freshrss.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then + ${pkgs.docker}/bin/docker network create ${networkName} + fi + ''; + }; + + virtualisation.oci-containers.containers.freshrss = { + hostname = "freshrss"; + image = "freshrss/freshrss:1.25.0"; + autoStart = true; + user = "0:33"; + ports = [ + "${toString port}:80/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + environment = { + TZ = config.time.timeZone; + CRON_MIN = "3,18,33,48"; # Alternatively, configure cron inside container. + SERVER_DNS = "rss.depeuter.dev"; + TRUSTED_PROXY = "172.16.0.1/12 192.168.0.1/16"; + }; + volumes = [ + "/srv/freshrss/www/freshrss/data:/var/www/FreshRSS/data" + "/srv/freshrss/www/freshrss/extensions:/var/www/FreshRSS/extensions" + ]; + labels = { + "traefik.enable" = "true"; + + "traefik.http.middlewares.freshrssM1.compress" = "true"; + "traefik.http.middlewares.freshrssM2.headers.browserXssFilter" = "true"; + "traefik.http.middlewares.freshrssM2.headers.forceSTSHeader" = "true"; + "traefik.http.middlewares.freshrssM2.headers.frameDeny" = "true"; + "traefik.http.middlewares.freshrssM2.headers.referrerPolicy" = "no-referrer-when-downgrade"; + "traefik.http.middlewares.freshrssM2.headers.stsSeconds" = "31536000"; + "traefik.http.routers.freshrss.entryPoints" = "websecure"; + "traefik.http.routers.freshrss.tls" = "true"; + + "traefik.http.services.freshrss.loadbalancer.server.port" = "80"; + "traefik.http.routers.freshrss.middlewares" = "freshrssM1,freshrssM2"; + "traefik.http.routers.freshrss.rule" = "Host(`rss.depeuter.dev`)"; + }; + }; + }; +} diff --git a/modules/apps/gitea/default.nix b/modules/apps/gitea/default.nix new file mode 100644 index 0000000..0361bd5 --- /dev/null +++ b/modules/apps/gitea/default.nix @@ -0,0 +1,673 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.gitea; + + networkName = "gitea"; + + UID = 3015; + GID = config.users.groups.apps.gid; + postgresPassword = "ChangeMe"; + repoDir = "/srv/git"; + webPort = 3000; + sshPort = 2222; + dbPort = 5432; + redisPort = 6379; + + title = "Hugo's Forge"; + slogan = "Forging ideas into reality."; + description = "Personal git server for projects that don't need collaboration."; +in { + options.homelab.apps.gitea.enable = lib.mkEnableOption "Gitea"; + + config = lib.mkIf cfg.enable { + homelab = { + users = { + apps.enable = true; + backup.enable = true; + }; + + virtualisation.containers.enable = true; + }; + + users.users.gitea = { + uid = lib.mkForce UID; + isSystemUser = true; + group = config.users.groups.apps.name; + home = "/var/empty"; + shell = null; + }; + + # Use filesystem mounts because rootless containers otherwise don't have access to the mount path (nested in docker directories). + # You could probably fix this by modifying the access rights on the path, but what would the point of that be? + fileSystems = { + # Mount options: + # - hard: retry requests indefinitely if the server becomes unresponsive. + # - nosuid: prevent set-user-id and set-group-id bits + "/srv/gitea-config" = { + device = "192.168.0.11:/mnt/SMALL/CONFIG/GITEA"; + fsType = "nfs"; + options = [ + "rw" + "nfsvers=4.2" + "async" "soft" "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" + "nosuid" + "tcp" + ]; + }; + + "/srv/gitea-git" = { + device = "192.168.0.11:/mnt/SMALL/DATA/GIT"; + fsType = "nfs"; + options = [ + "rw" + "nfsvers=4.2" + "async" "soft" "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" + "nosuid" + "tcp" + ]; + }; + }; + + # Make sure the Docker network exists. + systemd.services."docker-${networkName}-create-network" = { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "docker-gitea-db.service" + "docker-gitea.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then + ${pkgs.docker}/bin/docker network create ${networkName} + fi + ''; + }; + + virtualisation.oci-containers.containers = { + gitea-db = { + hostname = "gitea-db"; + image = "postgres:15.8-alpine"; + autoStart = true; + ports = [ + "5432:${toString dbPort}/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + environment = { + POSTGRES_PASSWORD = "ChangeMe"; + PGDATA = "/var/lib/postgresql/data/pgdata"; + }; + volumes = [ + "gitea-db:/var/lib/postgresql/data/pgdata" + ]; + }; + + gitea-redis = { + hostname = "gitea-redis"; + image = "redis:7.4.0-alpine3.20"; + autoStart = true; + ports = [ + "6379:${toString redisPort}/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + volumes = [ + "gitea-redis:/data" + ]; + }; + + gitea = { + hostname = "gitea"; + image = "codeberg.org/forgejo/forgejo:11.0.1-rootless"; + autoStart = true; + user = "${toString UID}:${toString GID}"; + ports = [ + "3000:${toString webPort}/tcp" + "2222:${toString sshPort}/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + dependsOn = [ + "gitea-db" + "gitea-redis" + ]; + volumes = [ + "/srv/gitea-config:/var/lib/gitea" + "/srv/gitea-git:/srv/git" + "/etc/timezone:/etc/timezone:ro" + "/etc/localtime:/etc/localtime:ro" + ]; + environmentFiles = [ + # NOTE Don't forget to create this file. + # TODO Put in place using age(nix)? + "/var/lib/gitea.env" + ]; + environment = { + # App name that shows in every page title. + FORGEJO__APP_NAME = title; + # Shows a slogan near the App name in every page title. + FORGEJO__APP_SLOGAN = slogan; + # Defines how the AppDisplayName should be presented. + #FORGEJO__APP_DISPLAY_NAME_FORMAT = ""; + # Will automaticaly detect the current user - but you can set it here. + FORGEJO__RUN_USER = "gitea"; + # Application run mode, affects performance and debugging: "dev" or "prod", default is + # "prod". Mode "dev" makes Gitea easier to develop and debug, values other than "dev" are + # treated as "prod" which is for production use. + FORGEJO__RUN_MODE = "prod"; + # The working directory. + #WORK_PATH = ""; + + # Disable SSH feature when not available. + FORGEJO__server__DISABLE_SSH = "false"; + # Whether to use the builltin SSH server or not. + FORGEJO__server__START_SSH_SERVER = "true"; + # Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER. + #FORGEJO__server__BUILTIN_SSH_SERVER_USER = "git"; + # Domain to be exposed in clone URL. + #FORGEJO__server__SSH_DOMAIN = ""; + # SSH username displayed in clone URLs. + #FORGEJO__server__SSH_USER = "git"; + # The network interface the builtin SSH server should listen on. + #FORGEJO__server__SSH_LISTEN_HOST = "ens18"; + # Port number to be exposed in clone URL. + FORGEJO__server__SSH_PORT = "22"; + # Port number the builtin SSH server should listen on. + FORGEJO__server__SSH_LISTEN_PORT = toString sshPort; + # Root path of SSH directory, default is '~/.ssh', but you have to use '/home/git/.ssh'. + FORGEJO__server__SSH_ROOT_PATH = "/var/lib/gitea/ssh"; + # Gitea will create a authorized_keys file by default when it is not using the internal ssh server + # If you intend to use the AuthorizedKeysCommand functionality then you should turn this off. + #FORGEJO__server__SSH_CREATE_AUTHORIZED_KEYS_FILE = "true"; + # Gitea will create a authorized_principals file by default when it is not using the internal ssh server + # If you intend to use the AuthorizedPrincipalsCommand functionality then you should turn this off. + #FORGEJO__server__SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE = "true"; + # For the built-in SSH server, choose the ciphers to support for SSH connections, + # for system SSH this setting has no effect + #FORGEJO__server__SSH_SERVER_CIPHERS = "chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com"; + # For the built-in SSH server, choose the key exchange algorithms to support for SSH connections, + # for system SSH this setting has no effect + #FORGEJO__server__SSH_SERVER_KEY_EXCHANGES = "curve25519-sha256, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1"; + # For the built-in SSH server, choose the MACs to support for SSH connections, + # for system SSH this setting has no effect + #FORGEJO__server__SSH_SERVER_MACS = "hmac-sha2-256-etm@openssh.com, hmac-sha2-256, hmac-sha1"; + # For the built-in SSH server, choose the keypair to offer as the host key + # The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub + # relative paths are made absolute relative to the %(APP_DATA_PATH)s + FORGEJO__server__SSH_SERVER_HOST_KEYS = "/var/lib/gitea/ssh/forgejo.ed25519"; + # Directory to create temporary files in when testing public keys using ssh-keygen, + # default is the system temporary directory. + #FORGEJO__server__SSH_KEY_TEST_PATH = ""; + # Use `ssh-keygen` to parse public SSH keys. The value is passed to the shell. By default, Gitea does the parsing itself. + #FORGEJO__server__SSH_KEYGEN_PATH = ""; + # Enable SSH Authorized Key Backup when rewriting all keys, default is false + FORGEJO__server__SSH_AUTHORIZED_KEYS_BACKUP = "false"; + # ... + # Enable exposure of SSH clone URL to anonymous visitors, default is false. + FORGEJO__server__EXPOSE_ANONYMOUS = "false"; + # ... + # Enables git-lfs support. true or false, default is false. + FORGEJO__server__LFS_START_SERVER = "false"; + # ... + + # Database to use. Either "mysql", "postgres" or "sqlite3". + FORGEJO__database__DB_TYPE = "postgres"; + FORGEJO__database__HOST = "gitea-db:${toString dbPort}"; + FORGEJO__database__NAME = "gitea"; + FORGEJO__database__USER = "gitea"; + FORGEJO__database__PASSWD = postgresPassword; + #FORGEJO__database__SCHEMA = ""; + #FORGEJO__database__SSL_MODE = "disable"; + + # Whether the installer is disabled (set to true to disable the installer). + #FORGEJO__security__INSTALL_LOCK = "false"; + # Global security key that will be used. + # This key is VERY IMPORTANT. If you lose it, the data encrypted by it can't be decrypted anymore. + #FORGEJO__security__SECRET_KEY = ""; + # Alternatively, specify the location of the secret key. + #FORGEJO__security__SECRET_KEY_URI = "file:/etc/gitea/secret_key"; + # ... + + # IF the camo is enabled. + #FORGEJO__camo__ENABLED = "false"; + # .... + + # Enables OAuth2 provider + FORGEJO__oauth2__ENABLED = "false"; + # ... + + # Root path for the log files - defaults to %(GITEA_WORK_DIR)/log + #FORGEJO__log__ROOT_PATH = ""; + # Either "console", "file" or "conn", default is "console" + FORGEJO__log__MODE = "file"; + # Either "Trace", "Debug", "Info", "Warn", "Error" or "None", default is "Info". + FORGEJO__log__LEVEL = "Warn"; + # ... + # Collect SSH logs (Creates logs from ssh git requests) + FORGEJO__log__ENABLE_SSH_LOG = "true"; + # ... + + # The path of git executable. If empty, Gitea searches through the PATH environment. + #FORGEJO__git__PATH = ""; + # ... + FORGEJO__git_0x2E_timeout__MIGRATE = "600"; + FORGEJO__git_0x2E_timeout__MIRROR = "600"; + + # Time limit to confirm account/email registration. + #FORGEJO__service__ACTIVE_CODE_LIVE_MINUTES = "180"; + # Time limit to perform the reset of a forgotten password. + #FORGEJO__service__RESET_PASSWD_CODE_LIVE_MINUTES = "180"; + # Whether a new user needs to confirm their email when registering. + FORGEJO__service__REGISTER_EMAIL_CONFIRM = "true"; + # Whether a new user needs to be confirmed manually after registration. + FORGEJO__service__REGISTER_MANUAL_CONFIRM = "true"; + # List of domain names that are allowed to be used to register on a Gitea instance, wildcard is supported. + #FORGEJO__service__EMAIL_DOMAIN_ALLOWLIST = ""; + # Comma-separated list of domain names that are not allowed to be used to register on a Gitea instance, wildcard is supported. + #FORGEJO__service__EMAIL_DOMAIN_BLOCKLIST = ""; + # Disallow registration, only allow admins to create accounts. + FORGEJO__service__DISABLE_REGISTRATION = "true"; + # Allow registration only using gitea itself, it works only when DISABLE_REGISTRATION is false. + FORGEJO__service__ALLOW_ONLY_INTERNAL_REGISTRATION = "true"; + # Allow registration only using third-party services, it works only when DISABLE_REGISTRATION is false. + FORGEJO__service__ALLOW_ONLY_EXTERNAL_REGISTRATION = "false"; + # User must sign in to view anything. + FORGEJO__service__REQUIRE_SIGNIN_VIEW = "false"; + # Mail notification + FORGEJO__service__ENABLE_NOTIFY_MAIL = "true"; + # This setting enables gitea to be signed in with HTTP BASIC Authentication using the user's password. + # If you set this to false you will not be able to access the tokens endpoints on the API with your password. + # Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token. + FORGEJO__service__ENABLE_BASIC_AUTHENTICATION = "false"; + # ... + # Enable captcha validation for registration. + FORGEJO__service__ENABLE_CAPTCHA = "true"; + # Enable this to require captcha validation for login. + FORGEJO__service__REQUIRE_CAPTCHA_FOR_LOGIN = "true"; + # Requires captcha for external registrations + #FORGEJO__service__REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA = "false"; + # Requires a password for external registrations. + #FORGEJO__service__REQUIRE_EXTERNAL_REGISTRATION_PASSWORD = "false"; + # Type of captcha you want to use. Options: image, recaptcha, hcaptcha, mcaptcha, cfturnstile. + FORGEJO__service__CAPTCHA_TYPE = "image"; + # ... + # Default value for KeepEmailPrivate + # Each new user will get the value of this setting copied into their profile + FORGEJO__service__DEFAULT_KEEP_EMAIL_PRIVATE = "true"; + # Default value for AllowCreateOrganization + # Every new user will have rights set to create organizations depending on this setting. + FORGEJO__service__DEFAULT_ALLOW_CREATE_ORGANIZATION = "true"; + # Default value for IsRestricted + # Every new user will have restricted permissions depending on this setting. + FORGEJO__service__DEFAULT_USER_IS_RESTRICTED = "false"; + # Users will be able to use dots when choosing their username. Disabling this is + # helpful if your usersare having issues with e.g. RSS feeds or advanced third-party + # extensions that use strange regex patterns. + FORGEJO__service__ALLOW_DOTS_IN_USERNAMES = "false"; + # Either "public", "limited" or "private", default is "public". + # Limited is for users visible only to signed users. + # Private is for users visible only to members of their organizations + # Public is for users visible for everyone + FORGEJO__service__DEFAULT_USER_VISIBILITY = "limited"; + # Set which visibility modes a user can have + FORGEJO__service__ALLOWED_USER_VISIBILITY_MODES = "public,limited,private"; + # Either "public", "limited" or "private", default is "public". + # Limited is for organizations visible only to signed users + # Private is for organizations visible only to members of the organization + # Public is for organizations visible to everyone + FORGEJO__service__DEFAULT_ORG_VISIBILITY = "limited"; + # Default value for DefaultOrgMemberVisible + # True will make the membership of the users visible when added to the organisation + FORGEJO__service__DEFAULT_ORG_MEMBER_VISIBLE = "false"; + # Default value for EnableDependencies + # Repositories will use dependencies by default depending on this setting + #FORGEJO__service__DEFAULT_ENABLE_DEPENDENCIES = "true"; + # Dependencies can be added from any repository where the user is granted access or only from the current repository depending on this setting. + #FORGEJO__service__ALLOW_CROSS_REPOSITORY_DEPENDENCIES = "true"; + # Default map service. No external API support has been included. A service has to allow + # searching using URL parameters, the location will be appended to the URL as escaped query parameter. + # Some example values are: + # - OpenStreetMap: https://www.openstreetmap.org/search?query= + # - Google Maps: https://www.google.com/maps/place/ + # - MapQuest: https://www.mapquest.com/search/ + # - Bing Maps: https://www.bing.com/maps?where1= + #FORGEJO__service__USER_LOCATION_MAP_URL = "https://www.openstreetmap.org/search?query="; + # Enable heatmap on users profiles. + FORGEJO__service__ENABLE_USER_HEATMAP = "true"; + # Enable Timetracking + FORGEJO__service__ENABLE_TIMETRACKING = "true"; + # Default value for EnableTimetracking + # Repositories will use timetracking by default depending on this setting + FORGEJO__service__DEFAULT_ENABLE_TIMETRACKING = "false"; + # Default value for AllowOnlyContributorsToTrackTime + # Only users with write permissions can track time if this is true + #FORGEJO__service__DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME = "true"; + # Value for the domain part of the user's email address in the git log if user + # has set KeepEmailPrivate to true. The user's email will be replaced with a + # concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. Default + # value is "noreply." + DOMAIN, where DOMAIN resolves to the value from server.DOMAIN + # Note: do not use the notation below + FORGEJO__service__NO_REPLY_ADDRESS = "noreply.depeuter.dev"; + # Show Registration button. + FOGEJO__service__SHOW_REGISTRATION_BUTTON = "false"; + # Show milestones dashboard page - a view of all the user's milestones. + #FORGEJO__service__SHOW_MILESTONES_DASHBOARD_PAGE = "true"; + # Default value for AutoWatchNewRepos + # When adding a repo to a team or creating a new repo all team members will watch the + # repo automatically if enabled + #FORGEJO__service__AUTO_WATCH_NEW_REPOS = "true"; + # Default value for AutoWatchOnChanges + # Make the user watch a repository When they commit for the first time + #FORGEJO__service__AUTO_WATCH_ON_CHANGES = "false"; + # Minimum amount of time a user must exist before comments are kept when the user is deleted. + #FORGEJO__service__USER_DELETE_WITH_COMMENTS_MAX_TIME = "0"; + # Valid site url schemes for user profiles + #FORGEJO__service__VALID_SITE_URL_SCHEMES = "http,https"; + + # Enable repository badges (via shields.io or a similar generator) + #FORGEJO__badges__ENABLED = "true"; + # ... + + # Root path for storing all repository data. By default, it is set to %(APP_DATA_PATH)s/gitea-repositories. + # A relative path is interpreted as _`AppWorkPath`_/%(ROOT)s + FORGEJO__repository__ROOT = repoDir; + # ... + # Force every new repository to be private. + FORGEJO__repository__FORCE_PRIVATE = "false"; + # Default private when creating a new repository with push-to-create. + FORGEJO__repository__DEFAULT_PUSH_TO_CREATE = "true"; + # ... + # Allow users to push local repositories to Forgejo and have them automatically created for a user. + FORGEJO__repository__ENABLE_PUSH_CREATE_USER = "true"; + # Allow users to push local repositories to Forgejo and have them automatically created for an org. + FORGEJO__repository__ENABLE_PUSH_CREATE_ORG = "false"; + # Comma separated list of globally disabled repo units. + FORGEJO__repository__DISABLED_REPO_UNITS = ""; + # Comma separated list of default new repo units. + FORGEJO__repository__DEFAULT_REPO_UNITS = "repo.code,repo.issues,repo.pulls,repo.releases,repo.actions"; + # Comma separated list of default forked repo units. + FORGEJO__repository__DEFAULT_FORK_REPO_UNITS = "repo.code,repo.pulls"; + # Prefix archive files by placing them in a directory named after the repository. + FORGEJO__repository__PREFIX_ARCHIVE_FILES = "true"; + # Disable migrating feature. + FORGEJO__repository__DISABLE_MIGRATIONS = "false"; + # Disable stars feature. + FORGEJO__repository__DISABLE_STARS = "true"; + # Disable repository forking. + #FORGEJO__repository__DISABLE_FORKS = "false"; + # The default branch name of new repositories + FORGEJO__repository__DEFAULT_BRANCH = "main"; + # ... + + # List of prefixes used in Pull Request title to mark them as Work In Progress (matched in a case-insensitive manner) + FORGEJO__repository_0x2E_pull_0X2D_request__WORK_IN_PROGRESS_PREFIXES = "WIP:,[WIP],WIP"; + # ... + # In the default merge message for squash commits walk all commits to include all authors in the Co-authored-by otherwise just use those in the limited list. + FORGEJO__repository_0x2E_pull_0X2D_request__DEFAULT_MERGE_MESSAGE_ALL_AUTHORS = "true"; + # ... + + # Enable cors headers (disabled by default) + FORGEJO__cors__ENABLED = "true"; + # list of requesting origins that are allowed, eg: "https://*.example.com". + FORGEJO__cors__ALLOW_DOMAINS = "https://git.depeuter.dev,http://192.168.0.24:${toString webPort}"; + + # Set the default theme for the Gitea install. + FORGEJO__ui__DEFAULT_THEME = "gitea-auto"; + # All available themes. Allow users to select personalized themes regardless of `DEFAULT_THEME`. + FORGEJO__ui__THEMES = "gitea-auto,gitea-light,gitea-dark,forgejo-auto,forgejo-light,forgejo-dark,forgejo-auto-deuteranopia-protanopia,forgejo-light-deuteranopia-protanopia,forgejo-dark-deuteranopia-protanopia,forgejo-auto-tritanopia,forgejo-light-tritanopia-forgejo-dark-tritanopia,github-auto,github,github-dark,edge-auto,edge-light,edge-dark,everforest-auto,everforest-light,everforest-dark,gruvbox-auto,gruvbox-light,gruvbox-dark,gruvbox-material-auto,grubox-material-dark,gruvbox-material-light,sonokai-andromeda,sonokai-atlantis,sonokai-espresso,sonokai-maia,sonokai-shusia,sonokai,catppuccin-frappe-green,catppuccin-frappe-teal,catppuccin-frappe-sky,catppuccin-frappe-sapphire,catppuccin-frappe-blue,catppuccin-frappe-lavender,catppuccin-macchiato-green,catppuccin-macchiato-teal,catppuccin-macchiato-sky,catppuccin-macchiato-sapphire,catppuccin-macchiato-blue,catppuccin-macchiato-lavender,catppuccin-mocha-green,catppuccin-mocha-teal,catppuccin-mocha-sky,catppuccin-mocha-sapphire,catppuccin-mocha-blue,catppuccin-mocha-lavender,nord,pitchblack,matrix,dark-arc"; + + FORGEJO__ui_0x2E_meta__AUTHOR = "${title} - ${slogan}"; + FORGEJO__ui_0x2E_meta__DESCRIPTION = description; + FORGEJO__ui_0x2E_meta__KEYWORDS = "git,self-hosted,projects,code"; + + # Whether to render SVG files as images. If SVG rendering is disabled, SVG files are displayed as text and cannot be embedded in markdown files as images. + FORGEJO__ui_0x2E_svg__ENABLE_RENDER = "true"; + + # ... + # Enables math inline and block detection + FORGEJO__markdown__ENABLE_MATH = "true"; + + # Define allowed algorithms and their minimum key length (use -1 to disable a type) + #FORGEJO__ssh__0x2E__minimum_key_sizes__ED25519 = "256"; + #FORGEJO__ssh__0x2E__minimum_key_sizes__ECDSA = "256"; + FORGEJO__ssh_0x2E_minimum_key_sizes__RSA = "-1"; + FORGEJO__ssh_0x2E_minimum_key_sizes__DSA = "-1"; + + # ... indexer + + # ... queue + + # Disallow regular (non-admin) users from creating organizations. + #FORGEJO__admin__DISABLE_REGULAR_ORG_CREATION = "false"; + # Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled + FORGEJO__admin__DEFAULT_EMAIL_NOTIFICATIONS = "enabled"; + # Send an email to all admins when a new user signs up to inform the admins about this act. Options: true, false + FORGEJO__admin__SEND_NOTIFICATION_EMAIL_ON_NEW_USER = "true"; + # Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future + # - deletion: a user cannot delete their own account + # - manage_ssh_keys: a user cannot configure ssh keys + # - manage_gpg_keys: a user cannot configure gpg keys + #FORGEJO__admin__USER_DISABLED_FEATURES = ""; + # Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior. + # - deletion: a user cannot delete their own account + # - manage_ssh_keys: a user cannot configure ssh keys + # - manage_gpg_keys: a user cannot configure gpg keys + #FORGEJO__admin__EXTERNAL_USER_DISABLE_FEATURES = ""; + + # Whether to allow signin in via OpenID + FORGEJO__openid__ENABLE_OPENID_SIGNIN = "false"; + # Whether to allow registering via OpenID + # Do not include to rely on rhw DISABLE_REGISTRATION setting + FORGEJO__openid__ENABLE_OPENID_SIGNUP = "false"; + # ... + + # ... oath2_client + + # ... webhook + + FORGEJO__mailer__ENABLED = "true"; + # Buffer length of channel, keep it as it is if you don't know what it is. + #FORGEJO__mailer__SEND_BUFFER_LEN = "100"; + # Prefix displayed before subject in mail. + #FORGEJO__mailer__SUBJECT_PREFIX = ""; + # Mail server protocol. One of "smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy" + FORGEJO__mailer__PROTOCOL = "smtps"; + # Mail server address + FORGEJO__mailer__SMTP_ADDR = "smtp.gmail.com"; + # Mail server port. If no protocol is specified, it will be inferred by this setting. + FORGEJO__mailer__SMTP_PORT = "465"; + # Enable HELO operation. Defaults to true. + #FORGEJO__mailer__ENABLE_HELO = "true"; + # Custom hostname fo the HELO operation. If no value is provided, one is retrieved from + # the system. + #FORGEJO__mailer__HELO_HOSTNAME = ""; + # If set to 'true', completely ignores server certificate validation errors. UNSAFE! + #FORGEJO__mailer__FORCE_TRUST_SERVER_CERT = "false"; + # Use client certificate in connection. + #FORGEJO__mailer__USE_CLIENT_CERT = "false"; + #FORGEJO__mailer__CLIENT_CERT_FILE = "custom/mailer/cert.pem"; + #FORGEJO__mailer__CLIENT_KEY_FILE = "custom/mailer/key.pem"; + # Mail from address, RFC 5322. This can be just an email address, or the + # `"Name" ` format. + FORGEJO__mailer__FROM = ''"${title}" ''; + # Sometimes it is helpful to use a different address on the envelope. Set this to use + # ENVELOPE_FROM as the from on the envelope. Set to `<>` to send an empty address. + #FORGEJO__mailer__ENVELOPE_FROM = ""; + # If gitea sends mails on behave of users, it will just use the name also displayed in the + # WebUI. If you want e.g. `Mister X (by CodeIt) `, set it to + # `{{ .DisplayName }} (by {{ .AppName }})`. + # Available Variables: `.DisplayName`, `.AppName` and `.Domain`. + #FORGEJO__mailer__FROM_DISPLAY_NAME_FORMAT = "{{ .DisplayName }}"; + # Mailer user name and password, if required by provider. + #FORGEJO__mailer__USER = ""; + # Use PASSWD = `your password` for quoting if you use special characters in the password. + #FORGEJO__mailer__PASSWD = ""; + # Send mails only in plain text, without HTML alternative + #FORGEJO__mailer__SEND_AS_PLAIN_TEXT = "false"; + # Specify an alternative sendmail binary + #FORGEJO__mailer__SENDMAIL_PATH = "sendmail"; + # Specify any extra sendmail arguments + # WARNING: if your sendmail program interprets options you should set this to "--" or terminate these args with "--" + #FORGEJO__mailer__SENDMAIL_ARGS = ""; + # Timeout for Sendmail + #FORGEJO__mailer__SENDMAIL_TIMEOUT = "5m"; + # convert \r\n to \n for Sendmail + #FORGEJO__mailer__SENDMAIL_CONVERT_CRLF = "true"; + + # ... email.incoming + + # Either "memory", "redis", "memcache", or "twoqueue". default is "memory" + FORGEJO__cache__ADAPTER = "redis"; + # For "memory" only, GC interval in seconds, default is 60. + #FORGEJO__cache__INTERVAL = "60"; + # For "redis" and "memcache", connection host address + # redis: `redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` (or `redis+cluster://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` for a Redis cluster) + # memcache: `127.0.0.1:11211` + # twoqueue: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000` + FORGEJO__cache__HOST = "redis://gitea-redis:${toString redisPort}/0?pool_size=100&idle_timeout=180s"; + # Time to keep items in cache if not used, default is 16 hours. + # Setting it to -1 disables caching + FORGEJO__cache__ITEM_TTL = "16h"; + # Time to keep items in cache if not used, default is 8760 hours. + # Setting it to -1 disables caching + FORGEJO__cache_0X2E_last_0X2D_commit__ITEM_TTL = "8760h"; + # Only enable the cache when repository's commits count great than + FORGEJO__cache_0X2E_last_0X2D_commit__COMMITS_COUNT = "100"; + + # Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres" + # Default is "memory". "db" will reuse the configuration in [database] + #FORGEJO__session__PROVIDER = "memory"; + # Provider config options + # memory: doesn't have any config yet + # file: session file path, e.g. `data/sessions` + # redis: `redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` (or `redis+cluster://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` for a Redis cluster) + # mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table` + #FORGEJO__session__PROVIDER_CONFIG = "data/sessions"; # Relative paths will be made absolute against _`AppWorkPath`_. + # Session cookie name + FORGEJO__session__COOKIE_NAME = "i_like_tibo"; + # If you use session in https only: true or false. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL. + FORGEJO__session__COOKIE_SECURE = "true"; + # Session GC time interval in seconds, default is 86400 (1 day) + #FORGEJO__session__GC_0X2E_INTERVAL_0X2E_TIME = "86400"; + # Session life time in seconds, default is 86400 (1 day) + #FORGEJO__session__SESSION_0X2E_LIFE_0X2E_TIME = "86400"; + # Cookie domain name. Default is empty + FORGEJO__session__DOMAIN = "git.depeuter.dev"; + # SameSite settings. Either "none", "lax", or "strict" + FORGEJO__session__SAME_SITE = "strict"; + + # How Gitea deals with missing repository avatars + # none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used + #FORGEJO__picture__REPOSITORY_AVATAR_FALLBACK = "none"; + #FORGEJO__picture__REPOSITORY_AVATAR_FALLBACK_IMAGE = "/img/repo_default.png"; + # Max Width and Height of uploaded avatars. + # This is to limit the amount of RAM used when resizing the image. + FORGEJO__picture__AVATAR_MAX_WIDTH = "10000"; + FORGEJO__picture__AVATAR_MAX_HEIGTH = "10000"; + # The multiplication factor for rendered avatar images. + # Larger values result in finer rendering on HiDPI devices. + #FORGEJO__picture__AVATAR_RENDERED_SIZE_FACTOR = "2"; + # Maximum allowed file size for uploaded avatars. + # This is to limit the amount of RAM used when resizing the image. + FORGEJO__picture__AVATAR_MAX_FILE_SIZE = "1048576"; + # If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. + #FORGEJO__picture__AVATAR_MAX_ORIGIN_SIZE = "262144"; + # Chinese users can choose "duoshuo" + # or a custom avatar source, like: http://cn.gravatar.com/avatar/ + #FORGEJO__picture__GRAVATAR_SOURCE = "gravatar"; + # This value will always be true in offline mode. + #FORGEJO__picture__DISABLE_GRAVATAR = "false"; + # Federated avatar lookup uses DNS to discover avatar associated. + # with emails, see https://www.libravatar.org + # This value will always be false in offline mode or when Gravatar is disabled. + #FORGEJO__picture__ENABLE_FEDERATED_AVATAR = "false"; + + # ... attachment + + # ... time + + # ... cron + + # Enables the mirror functionality. Set to **false** to disable all mirrors. Pre-existing mirrors remain valid but won't be updated; may be converted to regular repo. + FORGEJO__mirror__ENABLED = "true"; + # Disable the creation of **new** pull mirrors. Pre-existing mirrors remain valid. Will be ignored if `mirror.ENABLED` is `false`. + FORGEJO__mirror__DISABLE_NEW_PULL = "false"; + # Disable the creation of **new** push mirrors. Pre-existing mirrors remain valid. Will be ignored if `mirror.ENABLED` is `false`. + FORGEJO__mirror__DISABLE_NEW_PUSH = "false"; + # Default interval as a duration between each check + FORGEJO__mirror__DEFAULT_INTERVAL = "1h"; + # Min interval as a duration must be > 1m + FORGEJO__mirror__MIN_INTERVAL = "5m"; + + # ... api + + # ... i18n + + # .. highlight.mapping + + # Show version information about Gitea and Go in the footer + FORGEJO__other__SHOW_FOOTER_VERSION = "false"; + # Show template execution time in the footer + FORGEJO__other__SHOW_FOOTER_TEMPLATE_LOAD_TIME = "false"; + # Show the "powered by" text in the footer + FORGEJO__other__SHOW_FOOTER_POWERED_BY = "false"; + # Generate sitemap. Defaults to `true`. + FORGEJO__other__ENABLE_SITEMAP = "true"; + # Enable/Disable RSS/Atom feed + FORGEJO__other__ENABLE_FEED = "true"; + + # ... markup + + # ... metrics + + # ... migrations + + # ... f3 + + # Enable/Disable federation capabilities + FORGEJO__federation_ENABLED = "false"; + # ... + + # Enable/Disable package registry capabilities + FORGEJO__packages__ENABLED = "true"; + + # ... storage + + # Repo-archive storage will override storage. + #FORGEJO__repo_0X2D_archive__STORAGE_TYPE = "local"; + # Where your lfs files reside, default is data/lfs + FORGEJO__repo_0X2D_archive__PATH = ""; + # Override the minio base path if storage type is minio. + #FORGEJO__repo_0X2D_archive__MINIO_BASE_PATH = ""; + + # lfs storage will override storage. + #FORGEJO__lfs__STORAGE_TYPE = "local"; + # Where your lfs files reside, default is data/lfs + FORGEJO__lfs__PATH = ""; + # Override the minio base path if storage is set to minio. + #FORGEJO__lfs__MINIO_BASE_PATH = "lfs/"; + + # Enable the proxy, all requests to external via HTTP will be affected + FORGEJO__proxy__PROXY_ENABLED = "false"; + # Proxy server URL, support http://, https//, socks://, blank will follow environment http_proxy/https_proxy/no_proxy + #FORGEJO__proxy__PROXY_URL = ""; + # Comma separated list of host names requiring proxy. Glob patterns (*) are accepted; use ** to match all hosts. + #FORGEJO__proxy__PROXY_HOSTS = ""; + + # Enable/Disable actions capabilities + FORGEJO__actions__ENABLED = "true"; + # Default address to get action plugins, e.g. the default value means downloading from "https://code.forgejo.org/actions/checkout" for "uses: actions/checkout@v3" + #FORGEJO__actions__DEFAULT_ACTIONS_URL = "https://code.forgejo.org"; + # ... + }; + }; + }; + }; +} diff --git a/modules/apps/homepage/default.nix b/modules/apps/homepage/default.nix new file mode 100644 index 0000000..b34f32f --- /dev/null +++ b/modules/apps/homepage/default.nix @@ -0,0 +1,79 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.apps.homepage; + + PUID = toString config.users.users.homepage.uid; + PGID = toString config.users.groups.apps.gid; + + homepage-config = "/srv/homepage-config"; + + proxyNet = config.homelab.apps.traefik.sharedNetworkName; +in { + options.homelab.apps.homepage = { + enable = lib.mkEnableOption "homepage"; + port = lib.mkOption { + type = lib.types.int; + default = 3000; + description = "homepage WebUI port"; + }; + exposePort = lib.mkEnableOption "expose homepage port"; + }; + + config = lib.mkIf cfg.enable { + homelab = { + users.apps.enable = true; + virtualisation.containers.enable = true; + }; + + users.users.homepage = { + uid = lib.mkForce 3018; + isSystemUser = true; + group = config.users.groups.apps.name; + home = "/var/empty"; + shell = null; + }; + + fileSystems."${homepage-config}" = { + device = "192.168.0.11:/mnt/SMALL/CONFIG/HOMEPAGE"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "async" "soft" "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" + "nosuid" "tcp" + ]; + }; + + virtualisation.oci-containers.containers.homepage = let + host = "homepage.${config.networking.domain}"; + in { + hostname = "homepage"; + image = "ghcr.io/gethomepage/homepage:v1.10.1"; + autoStart = true; + user = "${toString PUID}:${toString PGID}"; + ports = lib.mkIf cfg.exposePort [ + "${toString cfg.port}:3000/tcp" + ]; + networks = [ + proxyNet + ]; + volumes = [ + "${homepage-config}:/app/config" + # "/var/run/docker.sock:/var/run/docker.sock:ro" # For docker integrations + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.homepage.rule" = "Host(`${host}`)"; + "traefik.http.services.homepage.loadbalancer.server.port" = toString cfg.port; + }; + environment = { + inherit PUID PGID; + + HOMEPAGE_ALLOWED_HOSTS = "${host},192.168.0.91:3000"; + }; + }; + }; +} diff --git a/modules/apps/jellyfin/default.nix b/modules/apps/jellyfin/default.nix new file mode 100644 index 0000000..011f56b --- /dev/null +++ b/modules/apps/jellyfin/default.nix @@ -0,0 +1,169 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.jellyfin; + + networkName = "jellyfin"; + inherit (config.homelab.fileSystems) media; + + UID = 3008; + GID = config.users.groups.media.gid; +in { + options.homelab.apps.jellyfin.enable = lib.mkEnableOption "Jellyfin using Docker"; + + config = lib.mkIf cfg.enable { + homelab = { + fileSystems.media.video = { + enable = true; + permissions = [ "read" ]; + }; + + users = { + apps.enable = true; + media.enable = true; + }; + virtualisation.containers.enable = true; + }; + + fileSystems = { + "/srv/audio" = { + device = "192.168.0.11:/mnt/SMALL/MEDIA/AUDIO"; + fsType = "nfs"; + options = [ + "ro" + "nfsvers=4.2" + "async" "soft" + "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" + "nosuid" "tcp" + ]; + }; + + "/srv/homevideo" = { + device = "192.168.0.11:/mnt/BIG/MEDIA/HOMEVIDEO/ARCHIVE"; + fsType = "nfs"; + options = [ + "ro" + "nfsvers=4.2" + "async" "soft" + "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" + "nosuid" "tcp" + ]; + }; + + "/srv/photo" = { + device = "192.168.0.11:/mnt/BIG/MEDIA/PHOTO/ARCHIVE"; + fsType = "nfs"; + options = [ + "ro" + "nfsvers=4.2" + "async" "soft" + "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" + "nosuid" "tcp" + ]; + }; + }; + + users.users.jellyfin = { + uid = lib.mkForce UID; + isSystemUser = true; + group = config.users.groups.apps.name; + extraGroups = [ + config.users.groups.media.name + ]; + home = "/var/empty"; + shell = null; + }; + + # Make sure the Docker network exists. + systemd.services."docker-${networkName}-create-network" = { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "docker-jellyfin.service" + "docker-feishin.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then + ${pkgs.docker}/bin/docker network create ${networkName} + fi + ''; + }; + + virtualisation.oci-containers.containers = { + jellyfin = { + hostname = "jellyfin"; + image = "jellyfin/jellyfin:10.10.7"; + user = "${toString UID}:${toString GID}"; + autoStart = true; + ports = [ + "8096:8096/tcp" + # "8920:8920/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + "--device=nvidia.com/gpu=all" # Equivalent to --gpus=all + ]; + volumes = [ + "jellyfin-config:/config" + "cache:/cache" + + "/srv/audio:/media/audio" + "${media.video.hostPath}:/media/video" + "/srv/homevideo:/media/homevideo" + "/srv/photo:/media/photo" + ]; + environment = { + # TODO + }; + }; + + jellyfin-vue = { + hostname = "jellyfin-vue"; + image = "ghcr.io/jellyfin/jellyfin-vue:unstable"; + autoStart = true; + ports = [ + "8080:80/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + labels = { + }; + }; + + feishin = let + feishinPort = "9180"; + in { + hostname = "feishin"; + image = "ghcr.io/jeffvli/feishin:0.19.0"; + autoStart = true; + ports = [ + "${feishinPort}:9180/tcp" # Web player (HTTP) + ]; + extraOptions = [ + "--network=${networkName}" + ]; + environment = { + # pre defined server name + SERVER_NAME = "Hugo"; + # When true AND name/type/url are set, only username/password can be toggled + SERVER_LOCK = "true"; + # Either "jellyfin" or "navidrome" + SERVER_TYPE = "jellyfin"; + # http://address:port + SERVER_URL= "https://jelly.depeuter.dev"; + TZ = config.time.timeZone; + }; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.feishin.rule" = "Host(`play.jelly.depeuter.dev`)"; + "traefik.http.services.feishin.loadbalancer.server.port" = feishinPort; + "traefik.tls.options.default.minVersion" = "VersionTLS13"; + }; + }; + }; + }; +} diff --git a/modules/apps/plex/default.nix b/modules/apps/plex/default.nix new file mode 100644 index 0000000..b307b86 --- /dev/null +++ b/modules/apps/plex/default.nix @@ -0,0 +1,69 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.apps.plex; +in { + options.homelab.apps.plex.enable = lib.mkEnableOption "Plex"; + + config = lib.mkIf cfg.enable { + homelab = { + users = { + apps.enable = true; + media.enable = true; + }; + fileSystems.media.video.enable = true; + virtualisation.containers.enable = true; + }; + + users.users.plex = { + uid = lib.mkForce 3009; + isSystemUser = true; + group = config.users.groups.apps.name; + extraGroups = [ + config.users.groups.media.name + ]; + home = "/var/empty"; + shell = null; + }; + + virtualisation.oci-containers.containers.plex = let + videoHostPath = config.homelab.fileSystems.media.video.hostPath; + in { + hostname = "plex"; + image = "plexinc/pms-docker:1.41.6.9685-d301f511a"; + autoStart = true; + ports = [ + "32400:32400/tcp" # Plex Media Server + "1900:1900/udp" # Plex DLNA Server + "32469:32469/tcp" # Plex DLNA Server + "32410:32410/udp" # GDM network discovery + "32412:32412/udp" # GDM network discovery + "32413:32413/udp" # GDM network discovery + "32414:32414/udp" # GDM network discovery + # "8324:8324/tcp" # Controlling Plex for Roku via Plex Companion + ]; + environment = { + #ADVERTISE_AP = "..."; # TODO Configure ip + ALLOWED_NETWORKS = "192.168.0.0/24,172.16.0.0/16"; + CHANGE_CONFIG_DIR_OWNERSHIP = "false"; + HOSTNAME = "Hugo-Plex"; + PLEX_CLAIM = "claim-d5MqsjMeCZrUF6oUvssr"; + PLEX_UID = toString config.users.users.plex.uid; + PLEX_GID = toString config.users.groups.media.gid; + TZ = config.time.timeZone; + }; + volumes = [ + # TODO Backup over NFS + "plex-config:/config" + "plex-transcode:/transcode" + + "${videoHostPath}:/data/video:ro" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.plex.rule" = "Host(`plex.depeuter.dev`)"; + "traefik.http.services.plex.loadbalancer.server.port" = "32400"; + }; + }; + }; +} diff --git a/modules/apps/solidtime/default.nix b/modules/apps/solidtime/default.nix new file mode 100644 index 0000000..725d32d --- /dev/null +++ b/modules/apps/solidtime/default.nix @@ -0,0 +1,278 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.solidtime; + + networkName = "solidtime"; + internalNetworkName = "solidtime-internal"; + proxyNet = config.homelab.apps.traefiik.sharedNetworkName; + + user = "1000:1000"; + + # dbExternalPort = ...; + dbInternalPort = 5432; + + gotenbergPort = 3000; + + inherit (config.virtualisation.oci-containers) containers; + + solidtimeImageName = "solidtime/solidtime"; + version = "0.10.0"; + solidtimeImage = "${solidtimeImageName}:${version}"; + solidtimeImageFile = pkgs.dockerTools.pullImage { + imageName = solidtimeImageName; + finalImageTag = version; + imageDigest = "sha256:817d3a366ecc39f0473d7154372afa82dd4e6e50c66d70be45804892c8421cbb"; + sha256 = "sha256-h5aCKaquUF/EVsOHaLOHrn1HAoXZYPhAbJ+e4cmjSA8="; + }; + + volumes = [ + "solidtime-storage:/var/www/html/storage" + "solidtime-logs:/var/www/html/storage/logs" + "solidtime-app:/var/www/html/storage/app" + ]; + + # laravel.env + laravelEnv = { + APP_NAME = "Solidtime"; + VITE_APP_NAME = laravelEnv.APP_NAME; + APP_ENV = "production"; + APP_DEBUG = "false"; + APP_URL = "http://localhost:${toString cfg.port}"; + APP_FORCE_HTTPS = "false"; + APP_ENABLE_REGISTRATION = "false"; + TRUSTED_PROXIES = "0.0.0.0/0,2000:0:0:0:0:0:0:0/3"; + + # Logging + LOG_CHANNEL = "stderr_daily"; + LOG_LEVEL = "debug"; + + # Database + DB_CONNECTION = "pgsql"; + DB_HOST = containers.solidtimeDb.hostname; + DB_PORT = toString dbInternalPort; + DB_SSL_MODE = "require"; + DB_DATABASE = "solidtime"; + DB_USERNAME = "solidtime"; + DB_PASSWORD = "ChangeMe"; + + # Mail + #MAIL_MAILER = "smtp"; + #MAIL_HOST = "smtp.gmail.com"; + #MAIL_PORT = "465"; + #MAIL_ENCRYPTION = "tls"; + #MAIL_FROM_ADDRESS = "no-reply@time.depeuter.dev"; + MAIL_FROM_NAME = laravelEnv.APP_NAME; + #MAIL_USERNAME = "kmtl.hugo@gmail.com"; + #MAIL_PASSWORD = "fhfxoequhhqidrhd"; + + # Queue + QUEUE_CONNECTION = "database"; + + # File storage + FILESYSTEM_DISK = "local"; + PUBLIC_FILESYSTEM_DISK = "public"; + + # Services + GOTENBERG_URL = "http://${containers.solidtimeGotenberg.hostname}:${toString gotenbergPort}"; + }; + +in { + options.homelab.apps.solidtime = { + enable = lib.mkEnableOption "Solidtime time tracker using Docker"; + port = lib.mkOption { + type = lib.types.int; + default = 8000; + description = "Solidtime WebUI port"; + }; + exposePort = lib.mkEnableOption "Expose Soldtime port"; + }; + + config = lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + # Make sure the Docker network exists. + systemd.services = { + "docker-${networkName}-create-network" = { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "${containers.solidtime.serviceName}.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then + ${pkgs.docker}/bin/docker network create ${networkName} + fi + ''; + }; + "docker-${internalNetworkName}-create-network" = { + description = "Create Docker network for ${internalNetworkName}"; + requiredBy = [ + "${containers.solidtime.serviceName}.service" + "${containers.solidtimeScheduler.serviceName}.service" + "${containers.solidtimeQueue.serviceName}.service" + "${containers.solidtimeDb.serviceName}.service" + "${containers.solidtimeGotenberg.serviceName}.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${internalNetworkName}; then + ${pkgs.docker}/bin/docker network create ${internalNetworkName} + fi + ''; + }; + }; + + virtualisation.oci-containers.containers = { + solidtime = { + hostname = "solidtime"; + image = solidtimeImage; + imageFile = solidtimeImageFile; + inherit user; + autoStart = true; + dependsOn = [ + "solidtimeDb" + ]; + ports = [ + # Open ports if you don't use Traefik + "${toString cfg.port}:8000" + ]; + networks = [ + networkName + internalNetworkName + ]; + extraOptions = [ + # Healthecks + # test: [ "CMD", "curl", "--fail", "http://localhost:8000/health-check/up" ] + ''--health-cmd=curl --fail http://localhost:8000/health-check/up'' + ]; + inherit volumes; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.solidtime.rule" = "Host(`time.${config.networking.hostName}.depeuter.dev`)"; + "traefik.http.services.solidtime.loadbalancer.server.port" = toString cfg.port; + }; + environmentFiles = [ + "/home/admin/.solidtime.env" + ]; + environment = laravelEnv // { + CONTAINER_MODE = "http"; + }; + }; + solidtimeScheduler = { + hostname = "scheduler"; + image = solidtimeImage; + imageFile = solidtimeImageFile; + inherit user; + autoStart = true; + dependsOn = [ + "solidtimeDb" + ]; + networks = [ + internalNetworkName + ]; + extraOptions = [ + # Healthchecks + # test: [ "CMD", "healthcheck" ] + ''--health-cmd="healthcheck"'' + ]; + inherit volumes; + environmentFiles = [ + "/home/admin/.solidtime.env" + ]; + environment = laravelEnv // { + CONTAINER_MODE = "scheduler"; + }; + }; + solidtimeQueue = { + hostname = "queue"; + image = solidtimeImage; + imageFile = solidtimeImageFile; + inherit user; + autoStart = true; + networks = [ + internalNetworkName + ]; + extraOptions = [ + # Healthchecks + # test: [ "CMD", "healthcheck" ] + ''--health-cmd="healthcheck"'' + ]; + inherit volumes; + dependsOn = [ + "solidtimeDb" + ]; + environmentFiles = [ + "/home/admin/.solidtime.env" + ]; + environment = laravelEnv // { + CONTAINER_MODE = "worker"; + WORKER_COMMAND = "php /var/www/html/artisan queue:work"; + }; + }; + solidtimeDb = let + imageName = "postgres"; + finalImageTag = "15"; + in { + hostname = "database"; + image = "${imageName}:${finalImageTag}"; + imageFile = pkgs.dockerTools.pullImage { + inherit imageName finalImageTag; + imageDigest = "sha256:98fe06b500b5eb29e45bf8c073eb0ca399790ce17b1d586448edc4203627d342"; + sha256 = "sha256-AZ4VkOlROX+nR/MjDjsA4xdHzmtKjiBAtsp2Q6IdOvg="; + }; + autoStart = true; + ports = [ + # "${toString dbExternalPort}:${toString dbInternalPort}" + ]; + networks = [ + internalNetworkName + ]; + extraOptions = [ + # Healthchecks + # test: - CMD - pg_isready - '-q' - '-d' - '${DB_DATABASE}' - '-U' - '${DB_USERNAME}' retries: 3 timeout: 5s + ''--health-cmd="pg_isready -q -d ${laravelEnv.DB_DATABASE} -U ${laravelEnv.DB_USERNAME}"'' + "--health-retries=3" + "--health-timeout=5s" + ]; + volumes = [ + "solidtime-db:/var/lib/postgresql/data" + ]; + environment = { + PGPASSWORD = laravelEnv.DB_PASSWORD; + POSTGRES_DB = laravelEnv.DB_DATABASE; + POSTGRES_USER = laravelEnv.DB_USERNAME; + POSTGRES_PASSWORD = laravelEnv.DB_PASSWORD; + }; + }; + solidtimeGotenberg = let + imageName = "gotenberg/gotenberg"; + finalImageTag = "8.26.0"; + in { + hostname = "gotenberg"; + image = "${imageName}:${finalImageTag}"; + imageFile = pkgs.dockerTools.pullImage { + inherit imageName finalImageTag; + imageDigest = "sha256:328551506b3dec3ff6381dd47e5cd72a44def97506908269e201a8fbfa1c12c0"; + sha256 = "sha256-1zz4xDAgXxHUnkCVIfjHTgXb82EFEx+5am6Cu9+eZj4="; + }; + autoStart = true; + networks = [ + internalNetworkName + ]; + extraOptions = [ + # Healthchecks + # test: [ "CMD", "curl", "--silent", "--fail", "http://localhost:3000/health" ] + ''--health-cmd="curl --silent --fail http://localhost:${toString gotenbergPort}/health"'' + ]; + }; + }; + }; +} + diff --git a/modules/apps/speedtest/default.nix b/modules/apps/speedtest/default.nix new file mode 100644 index 0000000..4c42c64 --- /dev/null +++ b/modules/apps/speedtest/default.nix @@ -0,0 +1,27 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.apps.speedtest; +in { + options.homelab.apps.speedtest.enable = lib.mkEnableOption "Speedtest"; + + config = lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + virtualisation.oci-containers.containers.speedtest = { + hostname = "speedtest"; + image = "openspeedtest/latest:v2.0.5"; + ports = [ + "3000:3000" + "3001:3001" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.speedtest.rule" = "Host(`speedtest.${config.networking.hostName}.${config.networking.domain}`)"; + "traefik.http.services.speedtest.loadbalancer.server.port" = "9090"; + "traefik.tls.options.default.minVersion" = "VersionTLS13"; + }; + autoStart = true; + }; + }; +} diff --git a/modules/apps/traefik/default.nix b/modules/apps/traefik/default.nix new file mode 100644 index 0000000..7f6ce38 --- /dev/null +++ b/modules/apps/traefik/default.nix @@ -0,0 +1,90 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.traefik; + + port = 8080; +in { + options.homelab.apps.traefik = { + enable = lib.mkEnableOption "Traefik Reverse Proxy"; + sharedNetworkName = lib.mkOption { + type = lib.types.str; + default = "traefik"; + description = "The name of the shared network to connect the container to."; + }; + }; + + config = lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + # Make sure the Docker network exists. + systemd.services."docker-${cfg.sharedNetworkName}-create-network" = { + description = "Create Docker network for ${cfg.sharedNetworkName}"; + requiredBy = [ + "docker-traefik.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${cfg.sharedNetworkName}; then + ${pkgs.docker}/bin/docker network create ${cfg.sharedNetworkName} + fi + ''; + }; + + virtualisation.oci-containers.containers.traefik = { + hostname = "traefik"; + image = "traefik:v3.4.3"; + autoStart = true; + ports = [ + "80:80/tcp" + "443:443/tcp" + "${toString port}:${toString port}/tcp" # Web UI (enabled by --api.insecure=true) + ]; + extraOptions = [ + "--network=${cfg.sharedNetworkName}" + ]; + environmentFiles = [ + /home/admin/.cloudflare.secret + ]; + cmd = [ + "--api.insecure=true" + + # Add Docker provider + "--providers.docker=true" + "--providers.docker.exposedByDefault=false" + + # Add web entrypoint + "--entrypoints.web.address=:80/tcp" + "--entrypoints.web.http.redirections.entrypoint.to=websecure" + "--entrypoints.web.http.redirections.entrypoint.scheme=https" + + # Add websecure entrypoint + "--entrypoints.websecure.address=:443/tcp" + "--entrypoints.websecure.http.tls=true" + "--entrypoints.websecure.http.tls.certResolver=letsencrypt" + "--entrypoints.websecure.http.tls.domains[0].main=depeuter.dev" + "--entrypoints.websecure.http.tls.domains[0].sans=*.depeuter.dev" + "--entrypoints.websecure.http.tls.domains[1].sans=*.${config.networking.hostName}.depeuter.dev" + + # Certificates + "--certificatesresolvers.letsencrypt.acme.dnschallenge=true" + "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare" + "--certificatesresolvers.letsencrypt.acme.email=tibo.depeuter@telenet.be" + "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" + ]; + volumes = [ + "letsencryp:/letsencrypt" + + "/var/run/docker.sock:/var/run/docker.sock:ro" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.traefik.rule" = "Host(`traefik.${config.networking.hostName}.depeuter.dev`)"; + "traefik.http.services.traefik.loadbalancer.server.port" = toString port; + }; + }; + }; +} diff --git a/modules/apps/vaultwarden/default.nix b/modules/apps/vaultwarden/default.nix new file mode 100644 index 0000000..4510299 --- /dev/null +++ b/modules/apps/vaultwarden/default.nix @@ -0,0 +1,656 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.vaultwarden; + + networkName = "vaultwarden"; +in { + options.homelab.apps.vaultwarden = { + enable = lib.mkEnableOption "Vaultwarden"; + port = lib.mkOption { + type = lib.types.int; + default = 10102; + description = "Vaultwarden WebUI port"; + }; + domain = lib.mkOption { + type = lib.types.string; + example = "https://vault.depeuter.dev"; + description = "Domain to configure Vaultwarden on"; + }; + name = lib.mkOption { + type = lib.types.string; + example = "Hugo's Vault"; + description = "Service name to use for invitations and mail"; + }; + }; + + config = lib.mkIf cfg.enable { + homelab = { + # Allow remote backups. + users.backup.enable = true; + + virtualisation.containers.enable = true; + }; + + # Make sure the Docker network exists. + systemd.services."docker-${networkName}-create-network" = { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "docker-vaultwarden-db.service" + "docker-vaultwarden.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then + ${pkgs.docker}/bin/docker network create ${networkName} + fi + ''; + }; + + virtualisation.oci-containers.containers = let + dbHostname = "vaultwarden-db"; + dbPort = 5432; + in { + vaultwardenDb = { + hostname = dbHostname; + image = "postgres:15.8-alpine"; + autoStart = true; + ports = [ + "${toString dbPort}:5432/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + environment = { + POSTGRES_PASSWORD = "ChangeMe"; + PGDATA = "/var/lib/postgresql/data/pgdata"; + }; + volumes = [ + "vaultwarden-db:/var/lib/postgresql/data" + ]; + }; + + vaultwarden = let + dataDir = "/data"; + in { + hostname = "vaultwarden"; + image = "vaultwarden/server:1.34.3-alpine"; + autoStart = true; + ports = [ + "${toString cfg.port}:80/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + dependsOn = [ + "vaultwardenDb" + ]; + volumes = [ + "vaultwarden:${dataDir}" + ]; + environmentFiles = [ + # NOTE Don't forget to create this file + # TODO Put in place using age(nix)? + "/var/lib/vaultwarden.env" + ]; + environment = { + #################### + ### Data folders ### + #################### + + ## Main data folder + DATA_FOLDER = dataDir; + + ## Individual folders, these override %DATA_FOLDER% + # ICON_CACHE_FOLDER=data/icon_cache + # ATTACHMENTS_FOLDER=data/attachments + # SENDS_FOLDER=data/sends + # TMP_FOLDER=data/tmp + + ## Templates data folder, by default uses embedded templates + ## Check source code to see the format + # TEMPLATES_FOLDER=data/templates + ## Automatically reload the templates for every request, slow, use only for development + # RELOAD_TEMPLATES=false + + ## Web vault settings + # WEB_VAULT_FOLDER=web-vault/ + # WEB_VAULT_ENABLED=true + + ######################### + ### Database settings ### + ######################### + + ## Database URL + ## When using SQLite, this is the path to the DB file, default to %DATA_FOLDER%/db.sqlite3 + # DATABASE_URL=data/db.sqlite3 + ## When using MySQL, specify an appropriate connection URI. + ## Details: https://docs.diesel.rs/2.1.x/diesel/mysql/struct.MysqlConnection.html + # DATABASE_URL=mysql://user:password@host[:port]/database_name + ## When using PostgreSQL, specify an appropriate connection URI (recommended) + ## or keyword/value connection string. + ## Details: + ## - https://docs.diesel.rs/2.1.x/diesel/pg/struct.PgConnection.html + ## - https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING + DATABASE_URL = "postgresql://vaultwarden:ChangeMe@${dbHostname}:${toString dbPort}/vaultwarden"; + + ## Enable WAL for the DB + ## Set to false to avoid enabling WAL during startup. + ## Note that if the DB already has WAL enabled, you will also need to disable WAL in the DB, + ## this setting only prevents Vaultwarden from automatically enabling it on start. + ## Please read project wiki page about this setting first before changing the value as it can + ## cause performance degradation or might render the service unable to start. + # ENABLE_DB_WAL=true + + ## Database connection retries + ## Number of times to retry the database connection during startup, with 1 second delay between each retry, set to 0 to retry indefinitely + # DB_CONNECTION_RETRIES=15 + + ## Database timeout + ## Timeout when acquiring database connection + # DATABASE_TIMEOUT=30 + + ## Database max connections + ## Define the size of the connection pool used for connecting to the database. + # DATABASE_MAX_CONNS=10 + + ## Database connection initialization + ## Allows SQL statements to be run whenever a new database connection is created. + ## This is mainly useful for connection-scoped pragmas. + ## If empty, a database-specific default is used: + ## - SQLite: "PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL;" + ## - MySQL: "" + ## - PostgreSQL: "" + # DATABASE_CONN_INIT="" + + ################# + ### WebSocket ### + ################# + + ## Enable websocket notifications + # ENABLE_WEBSOCKET=true + + ########################## + ### Push notifications ### + ########################## + + ## Enables push notifications (requires key and id from https://bitwarden.com/host) + ## Details about mobile client push notification: + ## - https://github.com/dani-garcia/vaultwarden/wiki/Enabling-Mobile-Client-push-notification + # PUSH_ENABLED=false + # PUSH_INSTALLATION_ID=CHANGEME + # PUSH_INSTALLATION_KEY=CHANGEME + + # WARNING: Do not modify the following settings unless you fully understand their implications! + # Default Push Relay and Identity URIs + # PUSH_RELAY_URI=https://push.bitwarden.com + # PUSH_IDENTITY_URI=https://identity.bitwarden.com + # European Union Data Region Settings + # If you have selected "European Union" as your data region, use the following URIs instead. + # PUSH_RELAY_URI=https://api.bitwarden.eu + # PUSH_IDENTITY_URI=https://identity.bitwarden.eu + + ##################### + ### Schedule jobs ### + ##################### + + ## Job scheduler settings + ## + ## Job schedules use a cron-like syntax (as parsed by https://crates.io/crates/cron), + ## and are always in terms of UTC time (regardless of your local time zone settings). + ## + ## The schedule format is a bit different from crontab as crontab does not contains seconds. + ## You can test the the format here: https://crontab.guru, but remove the first digit! + ## SEC MIN HOUR DAY OF MONTH MONTH DAY OF WEEK + ## "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri" + ## "0 30 * * * * " + ## "0 30 1 * * * " + ## + ## How often (in ms) the job scheduler thread checks for jobs that need running. + ## Set to 0 to globally disable scheduled jobs. + # JOB_POLL_INTERVAL_MS=30000 + ## + ## Cron schedule of the job that checks for Sends past their deletion date. + ## Defaults to hourly (5 minutes after the hour). Set blank to disable this job. + # SEND_PURGE_SCHEDULE="0 5 * * * *" + ## + ## Cron schedule of the job that checks for trashed items to delete permanently. + ## Defaults to daily (5 minutes after midnight). Set blank to disable this job. + # TRASH_PURGE_SCHEDULE="0 5 0 * * *" + ## + ## Cron schedule of the job that checks for incomplete 2FA logins. + ## Defaults to once every minute. Set blank to disable this job. + # INCOMPLETE_2FA_SCHEDULE="30 * * * * *" + ## + ## Cron schedule of the job that sends expiration reminders to emergency access grantors. + ## Defaults to hourly (3 minutes after the hour). Set blank to disable this job. + # EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE="0 3 * * * *" + ## + ## Cron schedule of the job that grants emergency access requests that have met the required wait time. + ## Defaults to hourly (7 minutes after the hour). Set blank to disable this job. + # EMERGENCY_REQUEST_TIMEOUT_SCHEDULE="0 7 * * * *" + ## + ## Cron schedule of the job that cleans old events from the event table. + ## Defaults to daily. Set blank to disable this job. Also without EVENTS_DAYS_RETAIN set, this job will not start. + # EVENT_CLEANUP_SCHEDULE="0 10 0 * * *" + ## Number of days to retain events stored in the database. + ## If unset (the default), events are kept indefinitely and the scheduled job is disabled! + # EVENTS_DAYS_RETAIN= + ## + ## Cron schedule of the job that cleans old auth requests from the auth request. + ## Defaults to every minute. Set blank to disable this job. + # AUTH_REQUEST_PURGE_SCHEDULE="30 * * * * *" + ## + ## Cron schedule of the job that cleans expired Duo contexts from the database. Does nothing if Duo MFA is disabled or set to use the legacy iframe prompt. + ## Defaults to every minute. Set blank to disable this job. + # DUO_CONTEXT_PURGE_SCHEDULE="30 * * * * *" + + ######################## + ### General settings ### + ######################## + + ## Domain settings + ## The domain must match the address from where you access the server + ## It's recommended to configure this value, otherwise certain functionality might not work, + ## like attachment downloads, email links and U2F. + ## For U2F to work, the server must use HTTPS, you can use Let's Encrypt for free certs + ## To use HTTPS, the recommended way is to put Vaultwarden behind a reverse proxy + ## Details: + ## - https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS + ## - https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples + ## For development + # DOMAIN=http://localhost + ## For public server + DOMAIN = cfg.domain; + ## For public server (URL with port number) + # DOMAIN=https://vw.domain.tld:8443 + ## For public server (URL with path) + # DOMAIN=https://domain.tld/vw + + ## Controls whether users are allowed to create Bitwarden Sends. + ## This setting applies globally to all users. + ## To control this on a per-org basis instead, use the "Disable Send" org policy. + # SENDS_ALLOWED=true + + ## HIBP Api Key + ## HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key + # HIBP_API_KEY= + + ## Per-organization attachment storage limit (KB) + ## Max kilobytes of attachment storage allowed per organization. + ## When this limit is reached, organization members will not be allowed to upload further attachments for ciphers owned by that organization. + # ORG_ATTACHMENT_LIMIT= + ## Per-user attachment storage limit (KB) + ## Max kilobytes of attachment storage allowed per user. + ## When this limit is reached, the user will not be allowed to upload further attachments. + # USER_ATTACHMENT_LIMIT= + ## Per-user send storage limit (KB) + ## Max kilobytes of send storage allowed per user. + ## When this limit is reached, the user will not be allowed to upload further sends. + # USER_SEND_LIMIT= + + ## Number of days to wait before auto-deleting a trashed item. + ## If unset (the default), trashed items are not auto-deleted. + ## This setting applies globally, so make sure to inform all users of any changes to this setting. + # TRASH_AUTO_DELETE_DAYS= + + ## Number of minutes to wait before a 2FA-enabled login is considered incomplete, + ## resulting in an email notification. An incomplete 2FA login is one where the correct + ## master password was provided but the required 2FA step was not completed, which + ## potentially indicates a master password compromise. Set to 0 to disable this check. + ## This setting applies globally to all users. + # INCOMPLETE_2FA_TIME_LIMIT=3 + + ## Disable icon downloading + ## Set to true to disable icon downloading in the internal icon service. + ## This still serves existing icons from $ICON_CACHE_FOLDER, without generating any external + ## network requests. $ICON_CACHE_TTL must also be set to 0; otherwise, the existing icons + ## will be deleted eventually, but won't be downloaded again. + # DISABLE_ICON_DOWNLOAD=false + + ## Controls if new users can register + SIGNUPS_ALLOWED = "false"; + + ## Controls if new users need to verify their email address upon registration + ## Note that setting this option to true prevents logins until the email address has been verified! + ## The welcome email will include a verification link, and login attempts will periodically + ## trigger another verification email to be sent. + SIGNUPS_VERIFY = "false"; + + ## If SIGNUPS_VERIFY is set to true, this limits how many seconds after the last time + ## an email verification link has been sent another verification email will be sent + # SIGNUPS_VERIFY_RESEND_TIME=3600 + + ## If SIGNUPS_VERIFY is set to true, this limits how many times an email verification + ## email will be re-sent upon an attempted login. + # SIGNUPS_VERIFY_RESEND_LIMIT=6 + + ## Controls if new users from a list of comma-separated domains can register + ## even if SIGNUPS_ALLOWED is set to false + # SIGNUPS_DOMAINS_WHITELIST=example.com,example.net,example.org + + ## Controls whether event logging is enabled for organizations + ## This setting applies to organizations. + ## Disabled by default. Also check the EVENT_CLEANUP_SCHEDULE and EVENTS_DAYS_RETAIN settings. + # ORG_EVENTS_ENABLED=false + + ## Controls which users can create new orgs. + ## Blank or 'all' means all users can create orgs (this is the default): + # ORG_CREATION_USERS= + ## 'none' means no users can create orgs: + # ORG_CREATION_USERS=none + ## A comma-separated list means only those users can create orgs: + # ORG_CREATION_USERS=admin1@example.com,admin2@example.com + + ## Invitations org admins to invite users, even when signups are disabled + # INVITATIONS_ALLOWED=true + ## Name shown in the invitation emails that don't come from a specific organization + INVITATION_ORG_NAME = cfg.name; + + ## The number of hours after which an organization invite token, emergency access invite token, + ## email verification token and deletion request token will expire (must be at least 1) + # INVITATION_EXPIRATION_HOURS=120 + + ## Controls whether users can enable emergency access to their accounts. + ## This setting applies globally to all users. + # EMERGENCY_ACCESS_ALLOWED=true + + ## Controls whether users can change their email. + ## This setting applies globally to all users + # EMAIL_CHANGE_ALLOWED=true + + ## Number of server-side passwords hashing iterations for the password hash. + ## The default for new users. If changed, it will be updated during login for existing users. + # PASSWORD_ITERATIONS=600000 + + ## Controls whether users can set password hints. This setting applies globally to all users. + # PASSWORD_HINTS_ALLOWED=true + + ## Controls whether a password hint should be shown directly in the web page if + ## SMTP service is not configured. Not recommended for publicly-accessible instances + ## as this provides unauthenticated access to potentially sensitive data. + SHOW_PASSWORD_HINT = "false"; + + ######################### + ### Advanced settings ### + ######################### + + ## Client IP Header, used to identify the IP of the client, defaults to "X-Real-IP" + ## Set to the string "none" (without quotes), to disable any headers and just use the remote IP + # IP_HEADER=X-Real-IP + + ## Icon service + ## The predefined icon services are: internal, bitwarden, duckduckgo, google. + ## To specify a custom icon service, set a URL template with exactly one instance of `{}`, + ## which is replaced with the domain. For example: `https://icon.example.com/domain/{}`. + ## + ## `internal` refers to Vaultwarden's built-in icon fetching implementation. + ## If an external service is set, an icon request to Vaultwarden will return an HTTP + ## redirect to the corresponding icon at the external service. An external service may + ## be useful if your Vaultwarden instance has no external network connectivity, or if + ## you are concerned that someone may probe your instance to try to detect whether icons + ## for certain sites have been cached. + # ICON_SERVICE=internal + + ## Icon redirect code + ## The HTTP status code to use for redirects to an external icon service. + ## The supported codes are 301 (legacy permanent), 302 (legacy temporary), 307 (temporary), and 308 (permanent). + ## Temporary redirects are useful while testing different icon services, but once a service + ## has been decided on, consider using permanent redirects for cacheability. The legacy codes + ## are currently better supported by the Bitwarden clients. + # ICON_REDIRECT_CODE=302 + + ## Cache time-to-live for successfully obtained icons, in seconds (0 is "forever") + ## Default: 2592000 (30 days) + # ICON_CACHE_TTL=2592000 + ## Cache time-to-live for icons which weren't available, in seconds (0 is "forever") + ## Default: 2592000 (3 days) + # ICON_CACHE_NEGTTL=259200 + + ## Icon download timeout + ## Configure the timeout value when downloading the favicons. + ## The default is 10 seconds, but this could be to low on slower network connections + # ICON_DOWNLOAD_TIMEOUT=10 + + ## Block HTTP domains/IPs by Regex + ## Any domains or IPs that match this regex won't be fetched by the internal HTTP client. + ## Useful to hide other servers in the local network. Check the WIKI for more details + ## NOTE: Always enclose this regex withing single quotes! + # HTTP_REQUEST_BLOCK_REGEX='^(192\.168\.0\.[0-9]+|192\.168\.1\.[0-9]+)$' + + ## Enabling this will cause the internal HTTP client to refuse to connect to any non global IP address. + ## Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block + # HTTP_REQUEST_BLOCK_NON_GLOBAL_IPS=true + + ## Client Settings + ## Enable experimental feature flags for clients. + ## This is a comma-separated list of flags, e.g. "flag1,flag2,flag3". + ## + ## The following flags are available: + ## - "autofill-overlay": Add an overlay menu to form fields for quick access to credentials. + ## - "autofill-v2": Use the new autofill implementation. + ## - "browser-fileless-import": Directly import credentials from other providers without a file. + ## - "fido2-vault-credentials": Enable the use of FIDO2 security keys as second factor. + # EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials + + ## Require new device emails. When a user logs in an email is required to be sent. + ## If sending the email fails the login attempt will fail!! + # REQUIRE_DEVICE_EMAIL=false + + ## Enable extended logging, which shows timestamps and targets in the logs + # EXTENDED_LOGGING=true + + ## Timestamp format used in extended logging. + ## Format specifiers: https://docs.rs/chrono/latest/chrono/format/strftime + # LOG_TIMESTAMP_FORMAT="%Y-%m-%d %H:%M:%S.%3f" + + ## Logging to Syslog + ## This requires extended logging + # USE_SYSLOG=false + + ## Logging to file + # LOG_FILE=/path/to/log + + ## Log level + ## Change the verbosity of the log output + ## Valid values are "trace", "debug", "info", "warn", "error" and "off" + ## Setting it to "trace" or "debug" would also show logs for mounted routes and static file, websocket and alive requests + ## For a specific module append a comma separated `path::to::module=log_level` + ## For example, to only see debug logs for icons use: LOG_LEVEL="info,vaultwarden::api::icons=debug" + LOG_LEVEL = "warn"; + + ## Token for the admin interface, preferably an Argon2 PCH string + ## Vaultwarden has a built-in generator by calling `vaultwarden hash` + ## For details see: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#secure-the-admin_token + ## If not set, the admin panel is disabled + ## New Argon2 PHC string + ## Note that for some environments, like docker-compose you need to escape all the dollar signs `$` with an extra dollar sign like `$$` + ## Also, use single quotes (') instead of double quotes (") to enclose the string when needed + # ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78' + ## Old plain text string (Will generate warnings in favor of Argon2) + # ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp + + ## Enable this to bypass the admin panel security. This option is only + ## meant to be used with the use of a separate auth layer in front + # DISABLE_ADMIN_TOKEN=false + + ## Number of seconds, on average, between admin login requests from the same IP address before rate limiting kicks in. + # ADMIN_RATELIMIT_SECONDS=300 + ## Allow a burst of requests of up to this size, while maintaining the average indicated by `ADMIN_RATELIMIT_SECONDS`. + # ADMIN_RATELIMIT_MAX_BURST=3 + + ## Set the lifetime of admin sessions to this value (in minutes). + # ADMIN_SESSION_LIFETIME=20 + + ## Allowed iframe ancestors (Know the risks!) + ## https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors + ## Allows other domains to embed the web vault into an iframe, useful for embedding into secure intranets + ## This adds the configured value to the 'Content-Security-Policy' headers 'frame-ancestors' value. + ## Multiple values must be separated with a whitespace. + # ALLOWED_IFRAME_ANCESTORS= + + ## Number of seconds, on average, between login requests from the same IP address before rate limiting kicks in. + # LOGIN_RATELIMIT_SECONDS=60 + ## Allow a burst of requests of up to this size, while maintaining the average indicated by `LOGIN_RATELIMIT_SECONDS`. + ## Note that this applies to both the login and the 2FA, so it's recommended to allow a burst size of at least 2. + # LOGIN_RATELIMIT_MAX_BURST=10 + + ## BETA FEATURE: Groups + ## Controls whether group support is enabled for organizations + ## This setting applies to organizations. + ## Disabled by default because this is a beta feature, it contains known issues! + ## KNOW WHAT YOU ARE DOING! + # ORG_GROUPS_ENABLED=false + + ## Increase secure note size limit (Know the risks!) + ## Sets the secure note size limit to 100_000 instead of the default 10_000. + ## WARNING: This could cause issues with clients. Also exports will not work on Bitwarden servers! + ## KNOW WHAT YOU ARE DOING! + # INCREASE_NOTE_SIZE_LIMIT=false + + ## Enforce Single Org with Reset Password Policy + ## Enforce that the Single Org policy is enabled before setting the Reset Password policy + ## Bitwarden enforces this by default. In Vaultwarden we encouraged to use multiple organizations because groups were not available. + ## Setting this to true will enforce the Single Org Policy to be enabled before you can enable the Reset Password policy. + # ENFORCE_SINGLE_ORG_WITH_RESET_PW_POLICY=false + + ######################## + ### MFA/2FA settings ### + ######################## + + ## Yubico (Yubikey) Settings + ## Set your Client ID and Secret Key for Yubikey OTP + ## You can generate it here: https://upgrade.yubico.com/getapikey/ + ## You can optionally specify a custom OTP server + # YUBICO_CLIENT_ID=11111 + # YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA + # YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify + + ## Duo Settings + ## You need to configure the DUO_IKEY, DUO_SKEY, and DUO_HOST options to enable global Duo support. + ## Otherwise users will need to configure it themselves. + ## Create an account and protect an application as mentioned in this link (only the first step, not the rest): + ## https://help.bitwarden.com/article/setup-two-step-login-duo/#create-a-duo-security-account + ## Then set the following options, based on the values obtained from the last step: + # DUO_IKEY= + # DUO_SKEY= + # DUO_HOST= + ## After that, you should be able to follow the rest of the guide linked above, + ## ignoring the fields that ask for the values that you already configured beforehand. + ## + ## If you want to attempt to use Duo's 'Traditional Prompt' (deprecated, iframe based) set DUO_USE_IFRAME to 'true'. + ## Duo no longer supports this, but it still works for some integrations. + ## If you aren't sure, leave this alone. + # DUO_USE_IFRAME=false + + ## Email 2FA settings + ## Email token size + ## Number of digits in an email 2FA token (min: 6, max: 255). + ## Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting! + # EMAIL_TOKEN_SIZE=6 + ## + ## Token expiration time + ## Maximum time in seconds a token is valid. The time the user has to open email client and copy token. + # EMAIL_EXPIRATION_TIME=600 + ## + ## Maximum attempts before an email token is reset and a new email will need to be sent. + # EMAIL_ATTEMPTS_LIMIT=3 + ## + ## Setup email 2FA regardless of any organization policy + # EMAIL_2FA_ENFORCE_ON_VERIFIED_INVITE=false + ## Automatically setup email 2FA as fallback provider when needed + # EMAIL_2FA_AUTO_FALLBACK=false + + ## Other MFA/2FA settings + ## Disable 2FA remember + ## Enabling this would force the users to use a second factor to login every time. + ## Note that the checkbox would still be present, but ignored. + # DISABLE_2FA_REMEMBER=false + ## + ## Authenticator Settings + ## Disable authenticator time drifted codes to be valid. + ## TOTP codes of the previous and next 30 seconds will be invalid + ## + ## According to the RFC6238 (https://tools.ietf.org/html/rfc6238), + ## we allow by default the TOTP code which was valid one step back and one in the future. + ## This can however allow attackers to be a bit more lucky with there attempts because there are 3 valid codes. + ## You can disable this, so that only the current TOTP Code is allowed. + ## Keep in mind that when a sever drifts out of time, valid codes could be marked as invalid. + ## In any case, if a code has been used it can not be used again, also codes which predates it will be invalid. + # AUTHENTICATOR_DISABLE_TIME_DRIFT=false + + ########################### + ### SMTP Email settings ### + ########################### + + ## Mail specific settings, set SMTP_FROM and either SMTP_HOST or USE_SENDMAIL to enable the mail service. + ## To make sure the email links are pointing to the correct host, set the DOMAIN variable. + ## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory + SMTP_HOST = "smtp.gmail.com"; + SMTP_FROM = "vault@depeuter.dev"; + SMTP_FROM_NAME = cfg.name; + # SMTP_USERNAME=username + # SMTP_PASSWORD=password + # SMTP_TIMEOUT=15 + + ## Choose the type of secure connection for SMTP. The default is "starttls". + ## The available options are: + ## - "starttls": The default port is 587. + ## - "force_tls": The default port is 465. + ## - "off": The default port is 25. + ## Ports 587 (submission) and 25 (smtp) are standard without encryption and with encryption via STARTTLS (Explicit TLS). Port 465 (submissions) is used for encrypted submission (Implicit TLS). + SMTP_SECURITY = "starttls"; + SMTP_PORT = "587"; + + # Whether to send mail via the `sendmail` command + # USE_SENDMAIL=false + # Which sendmail command to use. The one found in the $PATH is used if not specified. + # SENDMAIL_COMMAND="/path/to/sendmail" + + ## Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections. + ## Possible values: ["Plain", "Login", "Xoauth2"]. + ## Multiple options need to be separated by a comma ','. + SMTP_AUTH_MECHANISM = "Login"; + + ## Server name sent during the SMTP HELO + ## By default this value should be is on the machine's hostname, + ## but might need to be changed in case it trips some anti-spam filters + # HELO_NAME= + + ## Embed images as email attachments + # SMTP_EMBED_IMAGES=true + + ## SMTP debugging + ## When set to true this will output very detailed SMTP messages. + ## WARNING: This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting! + # SMTP_DEBUG=false + + ## Accept Invalid Certificates + ## DANGEROUS: This option introduces significant vulnerabilities to man-in-the-middle attacks! + ## Only use this as a last resort if you are not able to use a valid certificate. + ## If the Certificate is valid but the hostname doesn't match, please use SMTP_ACCEPT_INVALID_HOSTNAMES instead. + # SMTP_ACCEPT_INVALID_CERTS=false + + ## Accept Invalid Hostnames + ## DANGEROUS: This option introduces significant vulnerabilities to man-in-the-middle attacks! + ## Only use this as a last resort if you are not able to use a valid certificate. + # SMTP_ACCEPT_INVALID_HOSTNAMES=false + + ####################### + ### Rocket settings ### + ####################### + + ## Rocket specific settings + ## See https://rocket.rs/v0.5/guide/configuration/ for more details. + # ROCKET_ADDRESS=0.0.0.0 + ## The default port is 8000, unless running in a Docker container, in which case it is 80. + # ROCKET_PORT=8000 + # ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"} + }; + }; + }; + }; +} diff --git a/modules/default.nix b/modules/default.nix index e944ebe..08bcacd 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -2,6 +2,7 @@ imports = [ ./apps ./common + ./fileSystems ./services ./virtualisation ]; diff --git a/modules/fileSystems/default.nix b/modules/fileSystems/default.nix new file mode 100644 index 0000000..7c25689 --- /dev/null +++ b/modules/fileSystems/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./media + ]; +} diff --git a/modules/fileSystems/media/default.nix b/modules/fileSystems/media/default.nix new file mode 100644 index 0000000..41cb81f --- /dev/null +++ b/modules/fileSystems/media/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./video + ]; +} diff --git a/modules/fileSystems/media/video/default.nix b/modules/fileSystems/media/video/default.nix new file mode 100644 index 0000000..e46193c --- /dev/null +++ b/modules/fileSystems/media/video/default.nix @@ -0,0 +1,42 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.fileSystems.media.video; + + remotePath = "/mnt/SMALL/MEDIA/VIDEO"; + + maxPermissions = permissions: + if builtins.elem "write" permissions then "rw" + else "ro"; + permissionsOption = maxPermissions cfg.permissions; +in { + options.homelab.fileSystems.media.video = { + enable = lib.mkEnableOption "MEDIA/VIDEO dataset"; + hostPath = lib.mkOption { + type = lib.types.path; + default = "/srv/video"; + description = "Mountpath on host"; + }; + permissions = lib.mkOption { + type = lib.types.listOf (lib.types.enum [ "read" "write" ]); + default = [ "read" ]; + description = "Mount options permissions"; + }; + }; + + config = lib.mkIf cfg.enable { + fileSystems."${cfg.hostPath}" = { + device = "192.168.0.11:${remotePath}"; + fsType = "nfs"; + options = [ + permissionsOption + "auto" + "nfsvers=4.2" + "async" "soft" + "rsize=1048576" "wsize=1048576" + "timeo=600" "retry=50" "retrans=2" "actimeo=1800" "lookupcache=all" + "_netdev" "nosuid" "tcp" + ]; + }; + }; +} diff --git a/modules/services/actions/default.nix b/modules/services/actions/default.nix new file mode 100644 index 0000000..ea6b025 --- /dev/null +++ b/modules/services/actions/default.nix @@ -0,0 +1,49 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.services.actions; +in { + options.homelab.services.actions.enable = lib.mkEnableOption "Actions runner"; + + config = lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + services.gitea-actions-runner = { + instances.depeuter-dev = { + enable = true; + url = "https://git.depeuter.dev"; + tokenFile = "/etc/runner/depeuter-dev"; + name = config.networking.hostName; + labels = [ + "debian-11:docker://debian:11" + "debian-12:docker://debian:12" + "debian-latest:docker://debian:latest" + "docker:host" + "Linux:host" + "self-hosted:host" + "ubuntu-22.04:docker://ubuntu:22.04" + "ubuntu-24.04:docker://ubuntu:24.04" + "ubuntu-latest:docker://ubuntu:latest" + ]; + settings = { + cache.enabled = true; + container.privileged = true; + }; + hostPackages = with pkgs; [ + bash + cmake + coreutils + curl + docker + gawk + git + gnused + nodejs + openssh + wget + ]; + }; + }; + }; +} + diff --git a/modules/services/default.nix b/modules/services/default.nix index f7295a5..f70bc54 100644 --- a/modules/services/default.nix +++ b/modules/services/default.nix @@ -1,5 +1,6 @@ { imports = [ + ./actions ./openssh ]; } diff --git a/modules/virtualisation/containers/default.nix b/modules/virtualisation/containers/default.nix new file mode 100644 index 0000000..ed87990 --- /dev/null +++ b/modules/virtualisation/containers/default.nix @@ -0,0 +1,23 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.virtualisation.containers; +in { + options.homelab.virtualisation.containers.enable = lib.mkEnableOption "OCI containers"; + + config = lib.mkIf cfg.enable { + environment.systemPackages = with pkgs; [ + nfs-utils + ]; + + virtualisation = { + docker = { + enable = true; + enableOnBoot = true; + autoPrune.enable = true; + }; + + oci-containers.backend = "docker"; + }; + }; +} diff --git a/modules/virtualisation/default.nix b/modules/virtualisation/default.nix index 50418a4..2290be2 100644 --- a/modules/virtualisation/default.nix +++ b/modules/virtualisation/default.nix @@ -1,5 +1,6 @@ { imports = [ + ./containers ./guest ]; } diff --git a/users/apps/default.nix b/users/apps/default.nix new file mode 100644 index 0000000..e4f7011 --- /dev/null +++ b/users/apps/default.nix @@ -0,0 +1,18 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.users.apps; +in { + options.homelab.users.apps.enable = lib.mkEnableOption "user Apps"; + + config.users = lib.mkIf cfg.enable { + groups.apps.gid = lib.mkForce 568; + users.apps = { + uid = lib.mkForce 568; + isSystemUser = true; + group = config.users.groups.apps.name; + home = "/var/empty"; + shell = null; + }; + }; +} diff --git a/users/backup/default.nix b/users/backup/default.nix new file mode 100644 index 0000000..acae033 --- /dev/null +++ b/users/backup/default.nix @@ -0,0 +1,21 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.users.backup; +in { + options.homelab.users.backup.enable = lib.mkEnableOption "user Backup"; + + config = lib.mkIf cfg.enable { + users.users.backup = { + description = "Backup User"; + isNormalUser = true; + extraGroups = [ + "docker" # Allow access to the docker socket. + ]; + openssh.authorizedKeys.keys = [ + # Hugo + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICms6vjhE9kOlqV5GBPGInwUHAfCSVHLI2Gtzee0VXPh" + ]; + }; + }; +} diff --git a/users/default.nix b/users/default.nix index 26b5ff9..fe82019 100644 --- a/users/default.nix +++ b/users/default.nix @@ -1,5 +1,9 @@ { imports = [ ./admin + ./apps + ./backup + ./deploy + ./media ]; } diff --git a/users/deploy/default.nix b/users/deploy/default.nix new file mode 100644 index 0000000..0509d1e --- /dev/null +++ b/users/deploy/default.nix @@ -0,0 +1,49 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.users.deploy; +in { + options.homelab.users.deploy.enable = lib.mkEnableOption "user Deploy"; + + config = lib.mkIf cfg.enable { + users = { + groups.deploy = { }; + + # The user used to deploy rebuilds without password authentication + users.deploy = { + group = config.users.groups.deploy.name; + isSystemUser = true; + home = "/var/empty"; + shell = pkgs.bashInteractive; + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPrG+ldRBdCeHEXrsy/qHXIJYg8xQXVuiUR0DxhFjYNg" + ]; + }; + }; + + security.sudo.extraRules = [ + { + groups = [ + config.users.groups.deploy.name + ]; + commands = [ + { + command = "/nix/store/*-nix-*/bin/nix-env -p /nix/var/nix/profile/system --set /nix/store/*-*"; + options = [ "NOPASSWD" ]; + } + ]; + } + { + groups = [ + config.users.groups.deploy.name + ]; + commands = [ + { + command = "/nix/store/*/bin/switch-to-configuration"; + options = [ "NOPASSWD" ]; + } + ]; + } + ]; + }; +} diff --git a/users/media/default.nix b/users/media/default.nix new file mode 100644 index 0000000..e70a2b3 --- /dev/null +++ b/users/media/default.nix @@ -0,0 +1,18 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.users.media; +in { + options.homelab.users.media.enable = lib.mkEnableOption "user Media"; + + config.users = lib.mkIf cfg.enable { + groups.media.gid = lib.mkForce 3000; + users.media = { + uid = lib.mkForce 3001; + isSystemUser = true; + group = config.users.groups.media.name; + home = "/var/empty"; + shell = null; + }; + }; +}