diff --git a/flake.nix b/flake.nix index babbc7a..6e40530 100644 --- a/flake.nix +++ b/flake.nix @@ -1,26 +1,21 @@ { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - nextcloud = { - url = "github:onny/nixos-nextcloud-testumgebung"; - inputs.nixpkgs.follows = "nixpkgs"; - }; + flake-parts.url = "github:hercules-ci/flake-parts"; gtrackmap = { url = "github:gtrackmap/gtrackmap"; inputs.nixpkgs.follows = "nixpkgs"; }; }; - outputs = { self, nixpkgs, gtrackmap, ... }@attrs: { - nixosConfigurations.freun-dev = nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; - specialArgs = attrs; - modules = [ - ./hardware-configuration.nix - ./configuration.nix - ./services.nix - gtrackmap.nixosModules.x86_64-linux.default - ]; - }; - }; + outputs = { flake-parts, nixpkgs, ... }@inputs: ( + flake-parts.lib.mkFlake { inherit inputs; } { + nixosConfigurations = { + freun-dev = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + specialArgs = inputs; + modules = [ ./hosts/freun-dev ]; + }; + }; + } + ); } - diff --git a/configuration.nix b/hosts/freun-dev/configuration.nix similarity index 99% rename from configuration.nix rename to hosts/freun-dev/configuration.nix index 584c1a4..9fb7cb2 100644 --- a/configuration.nix +++ b/hosts/freun-dev/configuration.nix @@ -2,8 +2,7 @@ # your system. Help is available in the configuration.nix(5) man page # and in the NixOS manual (accessible by running `nixos-help`). -{ config, pkgs, self, ... }: - +{ pkgs, ... }: { nix = { settings = { @@ -146,4 +145,3 @@ # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html). system.stateVersion = "23.05"; # Did you read the comment? } - diff --git a/hosts/freun-dev/default.nix b/hosts/freun-dev/default.nix new file mode 100644 index 0000000..7b57421 --- /dev/null +++ b/hosts/freun-dev/default.nix @@ -0,0 +1,7 @@ +{ ... }: { + imports = [ + ./configuration.nix + ./hardware-configuration.nix + ./services.nix + ]; +} diff --git a/hardware-configuration.nix b/hosts/freun-dev/hardware-configuration.nix similarity index 100% rename from hardware-configuration.nix rename to hosts/freun-dev/hardware-configuration.nix diff --git a/hosts/freun-dev/services.nix b/hosts/freun-dev/services.nix new file mode 100644 index 0000000..f05ccfa --- /dev/null +++ b/hosts/freun-dev/services.nix @@ -0,0 +1,63 @@ +{ pkgs, ... }: +rec { + networking.firewall.allowedTCPPorts = [ 80 443 ]; + + services.caddy = { + enable = true; + enableReload = true; + email = "admin@pimeys.pm"; + }; + + services.postgresql.enable = true; + + virtualisation.podman = { + enable = true; + autoPrune.enable = true; + dockerCompat = true; + defaultNetwork.settings = { + # Required for container networking to be able to use names. + dns_enabled = true; + }; + }; + + virtualisation.oci-containers.backend = "podman"; + + networking.firewall = { + trustedInterfaces = [ "podman1" ]; + interfaces.podman1.allowedUDPPorts = [ 53 ]; + }; + + imports = [ + ../modules/services/vaultwarden.nix + ../modules/services/immich.nix + ../modules/services/syncthing.nix + ../modules/services/invidious.nix + ../modules/servies/grafana.nix + ../modules/servies/gtrackmap.nix + ../modules/services/owncast.nix + ../modules/services/hydra.nix + ../modules/services/wireguard.nix + ]; + + services.immich = { + enable = true; + fqdn = "img.freun.dev"; + data_dir = fileSystems.immich_data.mountPoint; + secrets = "/var/secrets/immich"; + }; + + fileSystems.immich_data = { + mountPoint = "/mnt/storage/immich"; + device = "//u407959.your-storagebox.de/backup/immich"; + fsType = "cifs"; + options = + let + # this line prevents hanging on network split + automount_opts = "x-systemd.automount,auto,x-systemd.device-timeout=5s,x-systemd.mount-timeout=5s"; + + in + [ "${automount_opts},credentials=/var/secrets/smb-storage" ]; + }; + + environment.systemPackages = [ pkgs.cifs-utils ]; +} diff --git a/grafana.nix b/modules/services/grafana.nix similarity index 100% rename from grafana.nix rename to modules/services/grafana.nix diff --git a/gtrackmap.nix b/modules/services/gtrackmap.nix similarity index 100% rename from gtrackmap.nix rename to modules/services/gtrackmap.nix diff --git a/hydra.nix b/modules/services/hydra.nix similarity index 100% rename from hydra.nix rename to modules/services/hydra.nix diff --git a/modules/services/immich.nix b/modules/services/immich.nix new file mode 100644 index 0000000..df67d19 --- /dev/null +++ b/modules/services/immich.nix @@ -0,0 +1,110 @@ +{ lib, config, ... }: +let + cfg = config.services.immich; +in +{ + options.services.immich = with lib; { + enable = mkEnableOption "Enable immich"; + + fqdn = mkOption { + type = types.str; + description = "FQDN to use for the immich server"; + }; + data_dir = mkOption { + type = types.str; + description = "The directory to store immich data in"; + }; + secrets = mkOption { + type = types.str; + description = "Path to file with secrets"; + }; + version = mkOption { + type = types.str; + default = "release"; + description = "The version (docker image tag) of immich to use"; + }; + mounts = mkOption { + type = types.listOf types.str; + description = "Additional mounts to add to the immich container"; + default = [ ]; + }; + port = mkOption { + type = types.int; + default = 2283; + description = "Port to expose the immich server on"; + }; + }; + + imports = [ + ../util/container-services.nix + ]; + + config = lib.mkIf cfg.enable rec { + container-services.immich = { + description = "Immich image server"; + services = { + server = { + image = "ghcr.io/immich-app/immich-server:${cfg.version}"; + environmentFiles = [ + cfg.secrets + ]; + volumes = [ + "${cfg.data_dir}:/usr/src/app/upload:rw" + "/etc/localtime:/etc/localtime:ro" + ] ++ cfg.mounts; + ports = [ "${builtins.toString cfg.port}:3001/tcp" ]; + dependsOn = [ + container-services.immich.services.redis + container-services.immich.services.postgres + ]; + }; + machine_learning = { + image = "ghcr.io/immich-app/immich-machine-learning:${cfg.version}"; + environmentFiles = [ + cfg.secrets + ]; + volumes = [ + "model_cache:/cache:rw" + ]; + }; + redis = { + image = "registry.hub.docker.com/library/redis:6.2-alpine"; + healthCheck.test = "redis-cli ping || exit 1"; + environmentFiles = [ + cfg.secrets + ]; + }; + postgres = { + image = "registry.hub.docker.com/tensorchord/pgvecto-rs:pg14-v0.2.0"; + environmentFiles = [ + cfg.secrets + ]; + environment = { + POSTGRES_INITDB_ARGS = "--data-checksums"; + }; + volumes = [ + "db_data:/var/lib/postgresql/data:rw" + ]; + cmd = [ "postgres" "-c" "shared_preload_libraries=vectors.so" "-c" "search_path=\"$user\", public, vectors" "-c" "logging_collector=on" "-c" "max_wal_size=2GB" "-c" "shared_buffers=512MB" "-c" "wal_compression=on" ]; + healthCheck = { + test = '' + pg_isready --dbname='$\{DB_DATABASE_NAME}' || exit 1 + Chksum="$(psql --dbname='$\{DB_DATABASE_NAME}' --username='$\{DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')" + echo "checksum failure count is $Chksum" + [ "$Chksum" = '0' ] || exit 1 + ''; + interval = "5m"; + startInterval = "30s"; + startPeriod = "5m"; + }; + }; + }; + }; + + services.caddy.virtualHosts = { + "${cfg.fqdn}".extraConfig = '' + reverse_proxy localhost:${builtins.toString cfg.port} + ''; + }; + }; +} diff --git a/invidious.nix b/modules/services/invidious.nix similarity index 100% rename from invidious.nix rename to modules/services/invidious.nix diff --git a/owncast.nix b/modules/services/owncast.nix similarity index 100% rename from owncast.nix rename to modules/services/owncast.nix diff --git a/syncthing.nix b/modules/services/syncthing.nix similarity index 100% rename from syncthing.nix rename to modules/services/syncthing.nix diff --git a/vaultwarden.nix b/modules/services/vaultwarden.nix similarity index 100% rename from vaultwarden.nix rename to modules/services/vaultwarden.nix diff --git a/wireguard.nix b/modules/services/wireguard.nix similarity index 100% rename from wireguard.nix rename to modules/services/wireguard.nix diff --git a/modules/util/container-services.nix b/modules/util/container-services.nix new file mode 100644 index 0000000..c5e4b22 --- /dev/null +++ b/modules/util/container-services.nix @@ -0,0 +1,224 @@ +{ pkgs, lib, config, ... }: +let + cfg = config.container-services; + + healthcheck = with pkgs.lib; { + test = mkOption { + type = types.oneOf types.str (types.listOf types.str); + }; + interval = mkOption { + type = types.str; + default = "30s"; + }; + startupInterval = mkOption { + type = types.str; + default = "30s"; + }; + startPeriod = mkOption { + type = types.str; + default = "0s"; + }; + }; + + service = with pkgs.lib; { + name = mkOption { + type = types.nilOr types.str; + default = nil; + }; + + alias = mkOption { + type = types.nilOr types.str; + default = nil; + }; + + dependsOn = mkOption { + type = types.listOf service; + default = [ ]; + }; + + image = mkOption { + type = types.str; + }; + + volumes = mkOption { + type = types.listOf types.str; + default = [ ]; + }; + + ports = mkOption { + type = types.listOf types.str; + default = [ ]; + }; + + environment = mkOption { + type = types.attrsOf types.str; + default = { }; + }; + + environmentFiles = mkOption { + type = types.listOf types.str; + default = [ ]; + }; + + cmd = mkOption { + type = types.nilOr (types.listOf types.str); + default = nil; + }; + + healthCheck = mkOption { + type = types.nilOr (types.submodule healthcheck); + default = nil; + }; + }; + + pod = with pkgs.lib; { + name = mkOption { + type = types.nilOr str; + default = nil; + }; + + description = mkOption { + type = types.nilOr str; + default = nil; + }; + + services = mkOption { + type = types.attrsOf (types.submodule service); + default = { }; + }; + }; + + backend = config.virtualisation.oci-containers.backend; + volumeName = pod: name: "${pod.name}-${name}"; + volumeServiceName = pod: name: "${backend}-volume-${volumeName pod name}"; + volumeServiceRef = pod: name: config.systemd.services."${backend}-volume-${volumeName pod name}".name + ".service"; + serviceName = pod: service: "${backend}-${pod.name}-${service.name}"; + serviceRef = pod: service: config.systemd.services."${backend}-${pod.name}-${service.name}".name + ".service"; + networkName = pod: "${pod.name}-default"; + networkServiceName = pod: "${backend}-network-${pod.name}"; + networkServiceRef = pod: config.systemd.services."${backend}-network-${pod.name}".name + ".service"; + podName = pod: "${backend}-pod-${pod.name}"; + podRef = pod: config.systemd.targets."${backend}-pod-${pod.name}".name + ".target"; + namedVolumes = service: lib.filter (volume: (! ((lib.hasPrefix "/" volume) || (lib.hasPrefix "./" volume)))) (lib.map (volume: lib.head (lib.splitString ":" volume)) service.volumes); + oneShotService = { pod, description, script }: { + path = [ (if backend == "podman" then pkgs.podman else pkgs.docker) ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + inherit description; + inherit script; + partOf = [ podRef pod ]; + wantedBy = [ podRef pod ]; + }; +in +{ + options = with pkgs.lib; { + container-services = mkOption { + type = types.attrsOf (types.submodule pod); + default = { }; + }; + }; + + config = { + container-services = lib.mapAttrs + (name: pod: { + name = pod.name or name; + services = lib.mapAttrs + (name: service: { + name = service.name or name; + }) + pod.services; + }) + cfg; + + systemd.targets = lib.mapAttrs' (_: pod: + lib.nameValuePair (podName pod) { + wantedBy = [ "multi-user.target" ]; + unitConfig = { + Description = pod.description or pod.name; + }; + } + ); + + systemd.services = lib.foldl + (services: pod: lib.mergeAttrsList [ + services + { + "${networkServiceName pod}" = oneShotService { + description = "Network for ${pod.name}"; + script = '' + ${backend} network inspect ${networkName pod} || ${backend} network create ${networkName pod} + ''; + }; + } + (lib.mapAttrs' + (_: service: + let + dependencies = + (lib.map (service: serviceRef pod.name service.name) service.dependsOn) ++ + (lib.map (name: volumeServiceRef pod.name name) (namedVolumes service)) ++ + (networkServiceRef pod); + in + lib.nameValuePair (serviceName pod service) { + serviceConfig = { + Restart = lib.mkOverride 500 "always"; + }; + after = dependencies; + requires = dependencies; + partOf = [ (podRef pod) ]; + wantedBy = [ (podRef pod) ]; + } + ) + pod.services) + (lib.listToAttrs (lib.flatten ( + lib.map + (service: ( + lib.map + (volume: { + name = volumeServiceName pod volume; + value = oneShotService { + description = "Volume ${volume} of ${pod.name}"; + script = '' + ${backend} volume inspect ${volumeName pod volume} || ${backend} volume create ${volumeName pod volume} + ''; + }; + }) + namedVolumes) + ) + pod.services))) + ]) + { } + cfg; + + virtualisation.oci-containers.containers = lib.foldl + (containers: pod: containers // ( + lib.mapAttrs' (_: service: + lib.nameValuePair (serviceName pod service) { + image = service.image; + volumes = service.volumes; + log-driver = "journal"; + ports = service.ports; + environment = service.environment; + environmentFiles = service.environmentFiles; + cmd = service.cmd; + extraOptions = [ + "--network-alias=${service.alias or service.name}" + "--network=${networkName pod}" + ] ++ (if (service.healthCheck != null) then [ + "--health-cmd=${ + if (builtins.isList service.healthCheck.test) + then builtins.toJSON service.healthCheck.test + else service.healthCheck.test + }" + "--health-interval=${service.healthCheck.interval}" + "--health-startup-interval=${service.healthCheck.startupInterval}" + "--health-start-period=${service.healthCheck.startPeriod}" + ] else [ ]); + } + ) + )) + { } + cfg; + }; +} diff --git a/services.nix b/services.nix deleted file mode 100644 index f823547..0000000 --- a/services.nix +++ /dev/null @@ -1,41 +0,0 @@ -{ ... }: -{ - networking.firewall.allowedTCPPorts = [ 80 443 ]; - - services.caddy = { - enable = true; - enableReload = true; - email = "admin@pimeys.pm"; - }; - - services.postgresql.enable = true; - - virtualisation.podman = { - enable = true; - autoPrune.enable = true; - dockerCompat = true; - defaultNetwork.settings = { - # Required for container networking to be able to use names. - dns_enabled = true; - }; - }; - - virtualisation.oci-containers.backend = "podman"; - - networking.firewall = { - trustedInterfaces = [ "podman1" ]; - interfaces.podman1.allowedUDPPorts = [ 53 ]; - }; - - imports = [ - ./vaultwarden.nix - ./immich.nix - ./syncthing.nix - ./invidious.nix - ./grafana.nix - ./gtrackmap.nix - ./owncast.nix - ./hydra.nix - ./wireguard.nix - ]; -}