
[{"content":" Using Valkey for cache # As the official Valkey operator for Kubernetes is still in its early development stages at the time of writing, the next best way we can generalise the deployment of Valkey is to create a factory module which deploys the official Valkey Helm chart. This can then be used across multiple service modules.\nCreating the factory module # Using the dendritic pattern, we can easily create a NixOS factory module to handle the deployment of the Valkey helm chart:\nNote See NixOS Documentation for more information\n{ self, inputs, ... }: { config.flake.factory.valkey = { namespace, values, }: { lib, pkgs, config, ... }: let chart = { name = \u0026#34;valkey\u0026#34;; repo = \u0026#34;https://valkey.io/valkey-helm\u0026#34;; version = \u0026#34;0.9.3\u0026#34;; hash = \u0026#34;sha256-Ig2kNNiZka/DUSBHQB7fZq/+9sf6hrUeBveNolbxDvw=\u0026#34;; }; image = pkgs.dockerTools.pullImage { imageName = \u0026#34;valkey/valkey\u0026#34;; imageDigest = \u0026#34;sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63\u0026#34;; sha256 = \u0026#34;sha256-Fytwh9dNSRODr0ZsSaqIXGppqVF424C2TW47Uiv0ZWA=\u0026#34;; finalImageTag = \u0026#34;9.0.1\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; in { config = { services.k3s = { images = [ image ]; autoDeployCharts = { \u0026#34;${namespace}-valkey\u0026#34; = chart // { targetNamespace = namespace; createNamespace = true; values = values // { image = { repository = image.imageName; tag = image.imageTag; }; }; }; }; }; }; }; } Deploying valkey caches # Using the valkey factory method we can add individual deployments for use in complex service deployments: { self, inputs, lib, ... }: { flake.modules.nixos.immich-valkey = self.factory.valkey { namespace = \u0026#34;immich\u0026#34;; values = { resources = { requests.cpu = \u0026#34;20m\u0026#34;; requests.memory = \u0026#34;64Mi\u0026#34;; limits.cpu = \u0026#34;100m\u0026#34;; limits.memory = \u0026#34;128Mi\u0026#34;; }; auth = { enabled = true; usersExistingSecret = \u0026#34;immich-secrets\u0026#34;; aclUsers.default = { permissions = \u0026#34;~* \u0026amp;* +@all\u0026#34;; passwordKey = \u0026#34;valkey-password\u0026#34;; }; }; }; }; } ","externalUrl":null,"permalink":"/docs/kubernetes/08-valkey-factory/","section":"Docs","summary":"","title":"Kubernetes: A Factory for Valkey","type":"docs"},{"content":" Deploying postgres databases using CloudnativePG # CloudnativePG provides an easy to use API to deploy postgres databases on our cluster drawing from a single image pool with easy backups and configuration.\nDeploying the cloudnativePG Helm chart and image catalog # Before using the cloudnativePG API we must deploy the official helm chart. We must also define a postgres image catalog for database deployments to use: { inputs, ... }: { flake.modules.nixos.postgres = { config, lib, pkgs, ... }: let chart = { name = \u0026#34;cloudnative-pg\u0026#34;; repo = \u0026#34;https://cloudnative-pg.github.io/charts\u0026#34;; version = \u0026#34;0.28.2\u0026#34;; hash = \u0026#34;sha256-Q8gCniyIUnz96N0Z2I/RIPZ1ZfV4iyE6N95D7pb2TmQ=\u0026#34;; }; image = pkgs.dockerTools.pullImage { imageName = \u0026#34;ghcr.io/cloudnative-pg/cloudnative-pg\u0026#34;; imageDigest = \u0026#34;sha256:0dfff19ba7b52ca25851a1010028b6940fff2e233290465af1cfb08a5f3f4661\u0026#34;; hash = \u0026#34;sha256-zt741Ql1ILjDLNQn8XzmTQmds4407P8h9xtlArL+fmA=\u0026#34;; finalImageTag = \u0026#34;1.29.1\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; postgresImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;ghcr.io/cloudnative-pg/postgresql\u0026#34;; imageDigest = \u0026#34;sha256:d879dfab951cb0eef9beac367f259d08ea1c04ae84699526854ff9ae478656be\u0026#34;; sha256 = \u0026#34;sha256-ngZW2rMlOaGm/VbSR2AHGCQis88zt+S2I++Oi6hqcKE=\u0026#34;; finalImageTag = \u0026#34;18.3-standard-trixie\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; in { options = { postgres.enable = lib.mkEnableOption \u0026#34;Cloudnative-pg helm chart on k3s\u0026#34;; }; config = lib.mkIf config.postgres.enable { services.k3s = { images = [ image postgresImage ]; autoDeployCharts = { cloudnative-pg = chart // { targetNamespace = \u0026#34;database\u0026#34;; createNamespace = true; values = { image = { repository = image.imageName; tag = image.imageTag; }; resources = { requests.cpu = \u0026#34;100m\u0026#34;; requests.memory = \u0026#34;128Mi\u0026#34;; limits.cpu = \u0026#34;200m\u0026#34;; limits.memory = \u0026#34;256Mi\u0026#34;; }; }; extraDeploy = [ { apiVersion = \u0026#34;postgresql.cnpg.io/v1\u0026#34;; kind = \u0026#34;ClusterImageCatalog\u0026#34;; metadata = { name = \u0026#34;postgresql-global\u0026#34;; }; spec = { images = [ { major = 18; image = \u0026#34;${postgresImage.imageName}:${postgresImage.imageTag}\u0026#34;; } ]; }; } ]; }; }; }; }; }; } Deploying postgres databases # Once installed, we can use CloudnativePG to define postgres databases for use in complex service deployments: { self, inputs, ... }: { flake.modules.nixos.immich-postgres = { config, lib, pkgs, ... }: { config = lib.mkIf config.immich.enable { services.k3s.autoDeployCharts.immich.extraDeploy = [ { apiVersion = \u0026#34;postgresql.cnpg.io/v1\u0026#34;; kind = \u0026#34;Cluster\u0026#34;; metadata = { namespace = \u0026#34;immich\u0026#34;; name = \u0026#34;immich-postgres\u0026#34;; }; spec = { instances = 1; imageCatalogRef = { apiGroup = \u0026#34;postgresql.cnpg.io\u0026#34;; kind = \u0026#34;ClusterImageCatalog\u0026#34;; name = \u0026#34;postgresql-global\u0026#34;; major = 18; }; storage.size = \u0026#34;8Gi\u0026#34;; managed.roles = [ { name = \u0026#34;immich\u0026#34;; passwordSecret.name = \u0026#34;immich-secrets\u0026#34;; superuser = true; login = true; } ]; bootstrap.initdb = { database = \u0026#34;immich\u0026#34;; owner = \u0026#34;immich\u0026#34;; secret.name = \u0026#34;immich-secrets\u0026#34;; }; resources = { requests.cpu = \u0026#34;200m\u0026#34;; requests.memory = \u0026#34;256Mi\u0026#34;; limits.cpu = \u0026#34;1000m\u0026#34;; limits.memory = \u0026#34;1Gi\u0026#34;; }; }; } ]; }; }; } ","externalUrl":null,"permalink":"/docs/kubernetes/07-cloud-native-postgres/","section":"Docs","summary":"","title":"Kubernetes: Adding CloudnativePG Databases","type":"docs"},{"content":" Accessing the Cluster Remotely with Netbird # To access the cluster from outside of the home network we can deploy the Netbird operator for Kubernetes. Similar to tailscale, this allows us to access all of our services through a private mesh network. The operator handles the creation of network router nodes to create secure Wireguard tunnels to our exposed services.\nPrerequisites # Using the Netbird dashboard, a DNS zone, named \u0026ldquo;homelab\u0026rdquo; in this case, must be creted along with a srevice user api key with admin permissions.\nDeploying Netbird # Helm # We can use the official Helm chart to install the Netbird Operator: { self, inputs, ... }: { flake.modules.nixos.netbird-operator-charts = { config, lib, pkgs, ... }: let netbirdOperatorChart = { name = \u0026#34;netbird-operator\u0026#34;; repo = \u0026#34;oci://ghcr.io/netbirdio/helm-charts/netbird-operator\u0026#34;; version = \u0026#34;0.4.1\u0026#34;; hash = \u0026#34;sha256-gRdZViio1QZYNlaEEKk/36F0wc3MnMgtTZE2HTIx4qo=\u0026#34;; }; in { config = lib.mkIf config.netbird-operator.enable { services.k3s.autoDeployCharts = { netbird-operator = netbirdOperatorChart // { targetNamespace = \u0026#34;netbird\u0026#34;; createNamespace = true; }; }; }; }; } Preloading the Operator and Router Images # The images can be preloaded as usual with their corresponding helm values set: { self, inputs, ... }: { flake.modules.nixos.netbird-operator-images = { config, lib, pkgs, ... }: let operatorImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;ghcr.io/netbirdio/netbird-operator\u0026#34;; imageDigest = \u0026#34;sha256:0f89a7385eadfde8a47adbfa0ee7913e8d0b938293e08615ca0aa1deab91fcb4\u0026#34;; hash = \u0026#34;sha256-DYq4Gb7aGoH0L915fCf9f+gP17CZHfjbq8kR21/kuc4=\u0026#34;; finalImageTag = \u0026#34;v0.4.1\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; routerImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;ghcr.io/netbirdio/netbird\u0026#34;; imageDigest = \u0026#34;sha256:4c37fb63f33531fc0c5eec272839b0e4d290c595a0b15bf5fd9a288df1890392\u0026#34;; hash = \u0026#34;sha256-TDBO4bjEhbcj01z1ZdoSjfPS226LmC3bxOyyusFwh2M=\u0026#34;; finalImageTag = \u0026#34;0.71.0-rootless\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; in { config = lib.mkIf config.netbird-operator.enable { services.k3s = { images = [ operatorImage routerImage ]; autoDeployCharts.netbird-operator.values = { operator.image = { repository = operatorImage.imageName; tag = operatorImage.imageTag; }; routingClientImage = \u0026#34;${routerImage.imageName}:${routerImage.imageTag}\u0026#34;; }; }; }; }; } Configuring the Network Router # Using the netbird.io api we can deploy our network routers: { self, inputs, ... }: { flake.modules.nixos.netbird-operator-router = { config, lib, pkgs, ... }: { config = lib.mkIf config.netbird-operator.enable { services.k3s.autoDeployCharts.netbird-operator.extraDeploy = [ { apiVersion = \u0026#34;netbird.io/v1alpha1\u0026#34;; kind = \u0026#34;NetworkRouter\u0026#34;; metadata = { name = \u0026#34;homelab\u0026#34;; namespace = \u0026#34;netbird\u0026#34;; }; spec = { dnsZoneRef.name = \u0026#34;homelab\u0026#34;; workloadOverride = { replicas = 1; podTemplate.spec = { dnsConfig.options = [ { name = \u0026#34;ndots\u0026#34;; value = \u0026#34;0\u0026#34;; } ]; resources = { requests.cpu = \u0026#34;100m\u0026#34;; requests.memory = \u0026#34;128Mi\u0026#34;; limits.cpu = \u0026#34;250m\u0026#34;; limits.memory = \u0026#34;256Mi\u0026#34;; }; }; }; }; } ]; }; }; } Adding the Netbird Secret # Using sops-nix we can add a separate manifest to deploy the required Kubernetes secrets: { self, inputs, ... }: { flake.modules.nixos.netbird-operator-secrets = { config, lib, pkgs, ... }: { config = lib.mkIf (config.netbird-operator.enable \u0026amp;\u0026amp; config.secrets.enable \u0026amp;\u0026amp; config.secrets.netbird-operator.enable) { sops = { secrets = { \u0026#34;netbird/key\u0026#34; = { }; }; templates = { netbirdMgmtApiKey = { content = builtins.toJSON { apiVersion = \u0026#34;v1\u0026#34;; kind = \u0026#34;Secret\u0026#34;; metadata = { name = \u0026#34;netbird-mgmt-api-key\u0026#34;; namespace = \u0026#34;netbird\u0026#34;; }; type = \u0026#34;Opaque\u0026#34;; immutable = true; stringData = { NB_API_KEY = config.sops.placeholder.\u0026#34;netbird/key\u0026#34;; }; }; path = \u0026#34;/var/lib/rancher/k3s/server/manifests/netbird-mgmt-api-key.json\u0026#34;; }; }; }; }; }; } Exposing Services # Once installed, we can deploy NetworkResource manifests to expose Kubernetes services: { self, inputs, ... }: { flake.modules.nixos.immich-services = { config, lib, pkgs, ... }: { config = lib.mkIf config.immich.enable { services.k3s.autoDeployCharts.immich = { values.service.main = { type = \u0026#34;LoadBalancer\u0026#34;; annotations = { \u0026#34;metallb.io/address-pool\u0026#34; = \u0026#34;default\u0026#34;; \u0026#34;metallb.io/allow-shared-ip\u0026#34; = \u0026#34;immich\u0026#34;; \u0026#34;metallb.io/loadBalancerIPs\u0026#34; = \u0026#34;192.168.1.205\u0026#34;; }; }; extraDeploy = [ { apiVersion = \u0026#34;v1\u0026#34;; kind = \u0026#34;Service\u0026#34;; metadata = { name = \u0026#34;immich\u0026#34;; namespace = \u0026#34;immich\u0026#34;; }; spec = { type = \u0026#34;ClusterIP\u0026#34;; selector = { \u0026#34;app.kubernetes.io/controller\u0026#34; = \u0026#34;main\u0026#34;; \u0026#34;app.kubernetes.io/instance\u0026#34; = \u0026#34;immich\u0026#34;; \u0026#34;app.kubernetes.io/name\u0026#34; = \u0026#34;server\u0026#34;; }; ports = [ { name = \u0026#34;http\u0026#34;; port = 80; targetPort = 2283; protocol = \u0026#34;TCP\u0026#34;; } ]; }; } { apiVersion = \u0026#34;netbird.io/v1alpha1\u0026#34;; kind = \u0026#34;NetworkResource\u0026#34;; metadata = { name = \u0026#34;immich\u0026#34;; namespace = \u0026#34;immich\u0026#34;; }; spec = { networkRouterRef = { name = \u0026#34;homelab\u0026#34;; namespace = \u0026#34;netbird\u0026#34;; }; serviceRef = { name = \u0026#34;immich\u0026#34;; namespace = \u0026#34;immich\u0026#34;; }; groups = [ { name = \u0026#34;All\u0026#34;; } ]; }; } ]; }; }; }; } Managing the Cluster Network in Netbird # With our services deployed, we can manage the generate resources in the \u0026ldquo;homelab\u0026rdquo; network on the Netbird dashboard: ","externalUrl":null,"permalink":"/docs/kubernetes/06-netbird-operator/","section":"Docs","summary":"","title":"Kubernetes: Netbird Operator","type":"docs"},{"content":" Using MetalLB to expose services on the local network # MetalLB provides bare-metal load balancing which we can use to expose services on our cluster to IP addresses on our local network. It is of course important that the addresses used by MetalLB are not already in use by your\nDeploying MetalLB # Helm chart # First, we add modules to import the chart and set the namespace:\nNote The namespace must be set to metallb-system for the deployment to work\n{ self, inputs, ... }: { flake.modules.nixos.metallb-charts = { config, lib, pkgs, ... }: let metallbChart = { name = \u0026#34;metallb\u0026#34;; repo = \u0026#34;https://metallb.github.io/metallb\u0026#34;; version = \u0026#34;0.15.3\u0026#34;; hash = \u0026#34;sha256-J9t2HFrSUl/RMMkv4vLUUA+IcOQC/v48nLjTTYpxpww=\u0026#34;; }; in { config = lib.mkIf config.metallb.enable { services.k3s.autoDeployCharts = { metallb = metallbChart // { targetNamespace = \u0026#34;metallb-system\u0026#34;; createNamespace = true; }; }; }; }; } Images # Then we preload each image used by the Helm chart: { self, inputs, ... }: { flake.modules.nixos.metallb-images = { config, lib, pkgs, ... }: let controllerImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;quay.io/metallb/controller\u0026#34;; imageDigest = \u0026#34;sha256:6698ccc54c380913816ed1fd0758637ec87dd79da419c4ab170a2c26c158ab89\u0026#34;; hash = \u0026#34;sha256-j71loxJPSdOfqSGX3b5/4X7OtwhQlslAsSGnhpDlcZ8=\u0026#34;; finalImageTag = \u0026#34;v0.15.3\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; speakerImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;quay.io/metallb/speaker\u0026#34;; imageDigest = \u0026#34;sha256:c6a5b25b2e1fba610a57b2db4bb8141d7c133569d561a8cc29e38ca5113efbc4\u0026#34;; hash = \u0026#34;sha256-t2epMeOG1rfWG3juOMfEhb9bfzksvQ0SuwOLJKB1ob4=\u0026#34;; finalImageTag = \u0026#34;v0.15.3\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; in { config = lib.mkIf config.metallb.enable { services.k3s = { images = [ controllerImage speakerImage ]; autoDeployCharts.metallb.values = { controller.image = { repository = controllerImage.imageName; tag = controllerImage.imageTag; }; speaker.image = { repository = speakerImage.imageName; tag = speakerImage.imageTag; }; }; }; }; }; } Adding a default address pool and L2 advertisement # For MetalLB to work we must configure a pool of available IP Addresses and an L2 advertisement: { self, inputs, ... }: { flake.modules.nixos.metallb-settings = { config, lib, pkgs, ... }: { config = lib.mkIf config.metallb.enable { services.k3s.autoDeployCharts.metallb.extraDeploy = [ { apiVersion = \u0026#34;metallb.io/v1beta1\u0026#34;; kind = \u0026#34;IPAddressPool\u0026#34;; metadata = { name = \u0026#34;default\u0026#34;; namespace = \u0026#34;metallb-system\u0026#34;; }; spec = { addresses = [ \u0026#34;192.168.1.200-192.168.1.210\u0026#34; ]; autoAssign = true; }; } { apiVersion = \u0026#34;metallb.io/v1beta1\u0026#34;; kind = \u0026#34;L2Advertisement\u0026#34;; metadata = { name = \u0026#34;default\u0026#34;; namespace = \u0026#34;metallb-system\u0026#34;; }; spec = { ipAddressPools = [ \u0026#34;default\u0026#34; ]; }; } ]; }; }; } Exposing Services # Once deployed, services can be exposed using The LoadBalancer service type in Kubernetes manifests: { self, inputs, ... }: { flake.modules.nixos.flaresolverr-services = { config, lib, pkgs, ... }: { config = lib.mkIf (config.media-server.enable \u0026amp;\u0026amp; config.media-server.flaresolverr.enable) { services.k3s.manifests.flaresolverr.content = [ { apiVersion = \u0026#34;v1\u0026#34;; kind = \u0026#34;Service\u0026#34;; metadata = { name = \u0026#34;flaresolverr-lb\u0026#34;; namespace = \u0026#34;media\u0026#34;; annotations = { \u0026#34;metallb.io/address-pool\u0026#34; = \u0026#34;default\u0026#34;; \u0026#34;metallb.io/allow-shared-ip\u0026#34; = \u0026#34;media\u0026#34;; }; }; spec = { type = \u0026#34;LoadBalancer\u0026#34;; loadBalancerIP = \u0026#34;192.168.1.202\u0026#34;; selector = { \u0026#34;app\u0026#34; = \u0026#34;flaresolverr\u0026#34;; }; ports = [ { name = \u0026#34;http\u0026#34;; port = 8191; targetPort = 8191; protocol = \u0026#34;TCP\u0026#34;; } ]; }; } ]; }; }; } ","externalUrl":null,"permalink":"/docs/kubernetes/05-load-balancing/","section":"Docs","summary":"","title":"Kubernetes: MetalLB Load Balancing","type":"docs"},{"content":" Using Longhorn for cloud native persistent volumes # Longhorn provides cloud native, persistent volume provisioning with replication and easy backups. Perfect for a homelab, it can be easily deployed to our k3s cluster using the official Helm chart.\nDeploying Longhorn # Helm chart # First, we add modules to import the chart and set the namespace:\nNote The namespace must be set to longhorn-system for the deployment to work\n{ self, inputs, ... }: { flake.modules.nixos.longhorn-charts = { config, lib, pkgs, ... }: let longhornChart = { name = \u0026#34;longhorn\u0026#34;; repo = \u0026#34;https://charts.longhorn.io\u0026#34;; version = \u0026#34;1.11.2\u0026#34;; hash = \u0026#34;sha256-pwJyyDaDkj7ZyvoH/h5POm59XXSHQRGzqK1CHmQQKnc=\u0026#34;; }; in { config = lib.mkIf config.longhorn.enable { services.k3s.autoDeployCharts = { longhorn = longhornChart // { targetNamespace = \u0026#34;longhorn-system\u0026#34;; createNamespace = true; }; }; }; }; } Images # Then we preload each image used by the Helm chart: { self, inputs, ... }: { flake.modules.nixos.longhorn-images = { config, lib, pkgs, ... }: let csiAttacherImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;longhornio/csi-attacher\u0026#34;; imageDigest = \u0026#34;sha256:fe417c28a6b86f8e7e5d49fc223e22e9ab457f894d2c4a321932d136dc2c2530\u0026#34;; hash = \u0026#34;sha256-+defbWikOWKmZvwOX8vAy5cbwQa6EDiJOnfOLe4fzuo=\u0026#34;; finalImageTag = \u0026#34;v4.11.0-20260428\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; csiNodeDriverRegistrarImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;longhornio/csi-node-driver-registrar\u0026#34;; imageDigest = \u0026#34;sha256:e82a8c8f800d7fbb3c1edf3f90b557768091821a44d52280093394f7918ccb68\u0026#34;; hash = \u0026#34;sha256-tcltO1AEnHotTxyJ+N7FbzvBS2U1DgOrikGBB8lOA2k=\u0026#34;; finalImageTag = \u0026#34;v2.16.0-20260428\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; csiProvisionerImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;longhornio/csi-provisioner\u0026#34;; imageDigest = \u0026#34;sha256:9e519a21a77c060104716e1f98222bb46ab617778a3bfcd861c87119a8256764\u0026#34;; hash = \u0026#34;sha256-nUkForHWNHisq1cMNTjzJ3wuy7dmWBHSi/H2Zaa6ewA=\u0026#34;; finalImageTag = \u0026#34;v5.3.0-20260428\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; csiResizerImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;longhornio/csi-resizer\u0026#34;; imageDigest = \u0026#34;sha256:41cb674d1154e798aa2c20f53f72ee2a5597f1369bcad5878d1708aee47f6663\u0026#34;; hash = \u0026#34;sha256-BPXAG3NIacqmf3f8V4kjWZIfYw3Becdn8oCuNStGulE=\u0026#34;; finalImageTag = \u0026#34;v2.1.0-20260428\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; csiSnapshotterImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;longhornio/csi-snapshotter\u0026#34;; imageDigest = \u0026#34;sha256:1975fac3890f4e08b98792881cb597502112ce0eeeaaef383e52458c96db94c5\u0026#34;; hash = \u0026#34;sha256-tDLxZZUsFzerg1q0ofOD5IrdtxH2ywFFjJ3u/rG79xc=\u0026#34;; finalImageTag = \u0026#34;v8.5.0-20260428\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; csiLivenessProbeImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;longhornio/livenessprobe\u0026#34;; imageDigest = \u0026#34;sha256:eae162f7e70fb981f90d9206f299dddaf590c0c896cfb67acceca12cef526a44\u0026#34;; hash = \u0026#34;sha256-qZqaIl3LqLd0og1DEM5KwKh5P0lMlRZyUDIhyrFJ8L8=\u0026#34;; finalImageTag = \u0026#34;v2.18.0-20260428\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; longhornEngineImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;longhornio/longhorn-engine\u0026#34;; imageDigest = \u0026#34;sha256:7482e0437fbf475e1e32696fab22f47bf99b1ef8d067ffce9e34028347722628\u0026#34;; hash = \u0026#34;sha256-3ceS6YUy/h0W1Ofi0ZT9lafGf3MRWr6JZ1H/MqgM8t0=\u0026#34;; finalImageTag = \u0026#34;v1.11.2\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; longhornInstanceManagerImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;longhornio/longhorn-instance-manager\u0026#34;; imageDigest = \u0026#34;sha256:16dac125ef30bd3a375bc8ff7d10636ea0302d22d208c0cfb1be37ebb93ca30b\u0026#34;; hash = \u0026#34;sha256-gElWLr5Mk6ZPkUgsIDNHvuS53WXTC9Rn2YMUKdmQydI=\u0026#34;; finalImageTag = \u0026#34;v1.11.2\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; longhornManagerImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;longhornio/longhorn-manager\u0026#34;; imageDigest = \u0026#34;sha256:0f80ca11ac4eb7522f4e6e801a7afc9909ea8d3041575f3d029964c46590f096\u0026#34;; hash = \u0026#34;sha256-w2BvuR0oPN9H1fY3DYGd+5+pj0ECOmoEfuAMOnW7vjo=\u0026#34;; finalImageTag = \u0026#34;v1.11.2\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; longhornShareManagerImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;longhornio/longhorn-share-manager\u0026#34;; imageDigest = \u0026#34;sha256:c11559e998ea982e6bac1637d66cc2aaab662a6b546709f2e54e2bfa50ffb0c3\u0026#34;; hash = \u0026#34;sha256-1JByktvTg0MWqF4qGn74zmfX/az/GjMux0RrYxMRIE8=\u0026#34;; finalImageTag = \u0026#34;v1.11.2\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; longhornUiImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;longhornio/longhorn-ui\u0026#34;; imageDigest = \u0026#34;sha256:885bc78f99f31da0d9b0fd8f533a53558a3aa81f9719c62e0d3c69ed8456d5b7\u0026#34;; hash = \u0026#34;sha256-z8/YJtkoVM2CKnoO1yoJdl+OfKQWuiw8VNbipj2ZOUs=\u0026#34;; finalImageTag = \u0026#34;v1.11.2\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; in { config = lib.mkIf config.longhorn.enable { services.k3s = { images = [ csiAttacherImage csiNodeDriverRegistrarImage csiProvisionerImage csiResizerImage csiSnapshotterImage csiLivenessProbeImage longhornEngineImage longhornInstanceManagerImage longhornManagerImage longhornShareManagerImage longhornUiImage ]; autoDeployCharts.longhorn.values = { image = { csi = { attacher = { repository = csiAttacherImage.imageName; tag = csiAttacherImage.imageTag; }; nodeDriverRegistrar = { repository = csiNodeDriverRegistrarImage.imageName; tag = csiNodeDriverRegistrarImage.imageTag; }; provisioner = { repository = csiProvisionerImage.imageName; tag = csiProvisionerImage.imageTag; }; resizer = { repository = csiResizerImage.imageName; tag = csiResizerImage.imageTag; }; snapshotter = { repository = csiSnapshotterImage.imageName; tag = csiSnapshotterImage.imageTag; }; livenessProbe = { repository = csiLivenessProbeImage.imageName; tag = csiLivenessProbeImage.imageTag; }; }; longhorn = { engine = { repository = longhornEngineImage.imageName; tag = longhornEngineImage.imageTag; }; instanceManager = { repository = longhornInstanceManagerImage.imageName; tag = longhornInstanceManagerImage.imageTag; }; manager = { repository = longhornManagerImage.imageName; tag = longhornManagerImage.imageTag; }; shareManager = { repository = longhornShareManagerImage.imageName; tag = longhornShareManagerImage.imageTag; }; ui = { repository = longhornUiImage.imageName; tag = longhornUiImage.imageTag; }; }; }; }; }; }; }; } Configuring Longhorn for a single node configuration # As Longhorn defaults to a Highly Available configuration with three replicas of each service and persistent volume we must add a module to properly set each replication count to one. Additionally, we must update the reclaim policy so that volumes are not recreated upon system reboots and lower the minimum required storage required to 10% to make the most out of limited system storage: { self, inputs, ... }: { flake.modules.nixos.longhorn-settings = { config, lib, pkgs, ... }: { config = lib.mkIf config.longhorn.enable { services.k3s.autoDeployCharts.longhorn.values = { longhornUI.replicas = 1; defaultSettings = { defaultReplicaCount = 1; storageMinimalAvailablePercentage = 10; storageReservedPercentageForDefaultDisk = 10; }; csi = { attacherReplicaCount = 1; provisionerReplicaCount = 1; resizerReplicaCount = 1; snapshotterReplicaCount = 1; }; persistence = { defaultClassReplicaCount = 1; reclaimPolicy = \u0026#34;Retain\u0026#34;; }; }; }; }; } Adding persistent volume claims # Once Longhorn is deployed, it should automatically be set as the default storage class in k3s. This means persistent volumes can be defined in manifests as such: { self, inputs, ... }: { flake.modules.nixos.immich-persistence = { config, lib, pkgs, ... }: { config = lib.mkIf config.immich.enable { services.k3s.autoDeployCharts.immich = { values.immich.persistence.library.existingClaim = \u0026#34;immich-pvc\u0026#34;; extraDeploy = [ { apiVersion = \u0026#34;v1\u0026#34;; kind = \u0026#34;PersistentVolumeClaim\u0026#34;; metadata = { name = \u0026#34;immich-pvc\u0026#34;; namespace = \u0026#34;immich\u0026#34;; }; spec = { accessModes = [ \u0026#34;ReadWriteOnce\u0026#34; ]; resources = { requests = { storage = \u0026#34;25Gi\u0026#34;; }; }; }; } ]; }; }; }; } Accessing the dashboard # Longhorn provides and easy to use web frontend. Read the following sections on MetalLB and the Netbird Operator to see how to expose this service to the local network and private vpn respectively. ","externalUrl":null,"permalink":"/docs/kubernetes/04-longhorn-persistence/","section":"Docs","summary":"","title":"Kubernetes: Longhorn Persistence","type":"docs"},{"content":" Adding Secrets # Note See sops-nix documentation for more details.\nWe can make use of sops-nix templates to deploy Kubernetes secrets which can then be referenced by Helm charts and other manifests. This should be done for all sensitive data such as API keys and passwords: { self, inputs, ... }: { flake.modules.nixos.immich-secrets = { config, lib, pkgs, ... }: { config = lib.mkIf (config.immich.enable \u0026amp;\u0026amp; config.secrets.enable \u0026amp;\u0026amp; config.secrets.immich.enable) { sops = { secrets = { \u0026#34;immich/key\u0026#34; = { }; \u0026#34;immich/postgres_password\u0026#34; = { }; \u0026#34;immich/valkey_password\u0026#34; = { }; }; templates = { immich-secrets = { content = builtins.toJSON { apiVersion = \u0026#34;v1\u0026#34;; kind = \u0026#34;Secret\u0026#34;; metadata = { name = \u0026#34;immich-secrets\u0026#34;; namespace = \u0026#34;immich\u0026#34;; }; type = \u0026#34;Opaque\u0026#34;; stringData = { username = \u0026#34;immich\u0026#34;; password = config.sops.placeholder.\u0026#34;immich/postgres_password\u0026#34;; valkey-password = config.sops.placeholder.\u0026#34;immich/valkey_password\u0026#34;; }; }; path = \u0026#34;/var/lib/rancher/k3s/server/manifests/immich-secrets.json\u0026#34;; }; }; }; }; }; } ","externalUrl":null,"permalink":"/docs/kubernetes/03-adding-secrets/","section":"Docs","summary":"","title":"Kubernetes: Adding Secrets","type":"docs"},{"content":" Deploying Helm Charts # To install a Helm chart on NixOS we can use the services.k3s.autoDeployCharts.\u0026lt;chart\u0026gt; config value to define the chart to be imported, the namespace it is to be deployed to as well as the values to be passed to the chart. Images can also be preloaded using services.k3s.images config value.\nAdding the charts # First, we add a module to import the chart and set the namespace: { self, inputs, ... }: { flake.modules.nixos.netbird-operator-charts = { config, lib, pkgs, ... }: let netbirdOperatorChart = { name = \u0026#34;netbird-operator\u0026#34;; repo = \u0026#34;oci://ghcr.io/netbirdio/helm-charts/netbird-operator\u0026#34;; version = \u0026#34;0.4.1\u0026#34;; hash = \u0026#34;sha256-gRdZViio1QZYNlaEEKk/36F0wc3MnMgtTZE2HTIx4qo=\u0026#34;; }; in { config = lib.mkIf config.netbird-operator.enable { services.k3s.autoDeployCharts = { netbird-operator = netbirdOperatorChart // { targetNamespace = \u0026#34;netbird\u0026#34;; createNamespace = true; }; }; }; }; } Adding the images # Images used by the Helm chart can be loaded into k3s and set in the chart values as such: { self, inputs, ... }: { flake.modules.nixos.netbird-operator-images = { config, lib, pkgs, ... }: let operatorImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;ghcr.io/netbirdio/netbird-operator\u0026#34;; imageDigest = \u0026#34;sha256:0f89a7385eadfde8a47adbfa0ee7913e8d0b938293e08615ca0aa1deab91fcb4\u0026#34;; hash = \u0026#34;sha256-DYq4Gb7aGoH0L915fCf9f+gP17CZHfjbq8kR21/kuc4=\u0026#34;; finalImageTag = \u0026#34;v0.4.1\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; routerImage = pkgs.dockerTools.pullImage { imageName = \u0026#34;ghcr.io/netbirdio/netbird\u0026#34;; imageDigest = \u0026#34;sha256:4c37fb63f33531fc0c5eec272839b0e4d290c595a0b15bf5fd9a288df1890392\u0026#34;; hash = \u0026#34;sha256-TDBO4bjEhbcj01z1ZdoSjfPS226LmC3bxOyyusFwh2M=\u0026#34;; finalImageTag = \u0026#34;0.71.0-rootless\u0026#34;; arch = \u0026#34;amd64\u0026#34;; }; in { config = lib.mkIf config.netbird-operator.enable { services.k3s = { images = [ operatorImage routerImage ]; autoDeployCharts.netbird-operator.values = { operator.image = { repository = operatorImage.imageName; tag = operatorImage.imageTag; }; routingClientImage = \u0026#34;${routerImage.imageName}:${routerImage.imageTag}\u0026#34;; }; }; }; }; } Adding extra configuration settings using chart values # Extra chart values can be defined in their own module also: { self, inputs, ... }: { flake.modules.nixos.netbird-operator-settings = { config, lib, pkgs, ... }: { config = lib.mkIf config.netbird-operator.enable { services.k3s.autoDeployCharts.netbird-operator.values = { resources = { requests.cpu = \u0026#34;100m\u0026#34;; requests.memory = \u0026#34;128Mi\u0026#34;; limits.cpu = \u0026#34;250m\u0026#34;; limits.memory = \u0026#34;256Mi\u0026#34;; }; }; }; }; } Deploying Raw Manifests # To install a service using raw manifests on NixOS we can use the services.k3s.manifests.\u0026lt;service-name\u0026gt; config value to define the manifests to be deployed. Images can also be preloaded using services.k3s.images config value.\nCreating the namespace # First, a module should be added to create the necessary namespace in k3s: { self, inputs, ... }: { flake.modules.nixos.blog-namespace = { config, lib, pkgs, ... }: { config = lib.mkIf config.blog.enable { services.k3s.manifests.blog.content = [ { apiVersion = \u0026#34;v1\u0026#34;; kind = \u0026#34;Namespace\u0026#34;; metadata = { name = \u0026#34;website\u0026#34;; }; } ]; }; }; } Loading images # Second, a module importing the images into k3s should be added. In this case, we import the image generated by my blog flake: { self, inputs, ... }: { flake.modules.nixos.blog-images = { config, lib, pkgs, ... }: let blogImage = inputs.blog.packages.\u0026#34;x86_64-linux\u0026#34;.dockerImage; in { config = lib.mkIf config.blog.enable { services.k3s = { images = [ blogImage ]; }; }; }; } Adding deployment manifests # Finally, a deployment module is required where replica count, security context, containers and probes are defined: { self, inputs, ... }: { flake.modules.nixos.blog-deployment = { config, lib, pkgs, ... }: { config = lib.mkIf config.blog.enable { services.k3s.manifests.blog.content = [ { apiVersion = \u0026#34;apps/v1\u0026#34;; kind = \u0026#34;Deployment\u0026#34;; metadata = { name = \u0026#34;blog\u0026#34;; namespace = \u0026#34;website\u0026#34;; }; spec = { replicas = 1; selector.matchLabels.app = \u0026#34;blog\u0026#34;; template = { metadata.labels.app = \u0026#34;blog\u0026#34;; spec = { securityContext = { runAsUser = 1000; runAsGroup = 1000; fsGroup = 1000; }; containers = [ { name = \u0026#34;blog\u0026#34;; image = \u0026#34;${ (lib.lists.findSingle ( x: x ? imageName \u0026amp;\u0026amp; x.imageName == \u0026#34;blog\u0026#34; ) null null config.services.k3s.images).imageName }:${ (lib.lists.findSingle ( x: x ? imageName \u0026amp;\u0026amp; x.imageName == \u0026#34;blog\u0026#34; ) null null config.services.k3s.images).imageTag }\u0026#34;; imagePullPolicy = \u0026#34;Never\u0026#34;; ports = [ { containerPort = 8080; } ]; startupProbe = { httpGet = { path = \u0026#34;/\u0026#34;; port = 8080; }; failureThreshold = 30; periodSeconds = 5; }; readinessProbe = { httpGet = { path = \u0026#34;/\u0026#34;; port = 8080; }; initialDelaySeconds = 15; periodSeconds = 10; timeoutSeconds = 2; failureThreshold = 3; }; livenessProbe = { httpGet = { path = \u0026#34;/\u0026#34;; port = 8080; }; initialDelaySeconds = 30; periodSeconds = 20; timeoutSeconds = 2; failureThreshold = 3; }; resources = { requests.cpu = \u0026#34;20m\u0026#34;; requests.memory = \u0026#34;32Mi\u0026#34;; limits.cpu = \u0026#34;100m\u0026#34;; limits.memory = \u0026#34;128Mi\u0026#34;; }; } ]; }; }; }; } ]; }; }; } ","externalUrl":null,"permalink":"/docs/kubernetes/02-deploying-services/","section":"Docs","summary":"","title":"Kubernetes: Deploying Services","type":"docs"},{"content":" Introduction # This documentation follows on from my NixOS documentation and describes the creation of a single node k3s cluster using nix. A mixture of helm and raw manifests will be used but all will be defined in pure nix as part of my nix-config flake.\nEnabling k3s # To begin using Kubernetes on NixOS we can enable the k3s service along with a token secret: { inputs, ... }: { flake.modules.nixos.k3s = { config, lib, pkgs, ... }: { options = { k3s.enable = lib.mkEnableOption \u0026#34;k3s\u0026#34;; secrets.k3s.enable = lib.mkEnableOption \u0026#34;k3s token secret\u0026#34;; }; config = lib.mkMerge [ (lib.mkIf config.k3s.enable { services.k3s = { enable = true; images = [ config.services.k3s.package.airgap-images ]; extraFlags = [ \u0026#34;--embedded-registry\u0026#34; \u0026#34;--disable servicelb\u0026#34; \u0026#34;--disable traefik\u0026#34; \u0026#34;--disable local-storage\u0026#34; \u0026#34;--disable metrics-server\u0026#34; ]; }; }) (lib.mkIf (config.k3s.enable \u0026amp;\u0026amp; config.secrets.enable \u0026amp;\u0026amp; config.secrets.k3s.enable) { sops.secrets.\u0026#34;k3s/token\u0026#34; = { }; services.k3s.tokenFile = config.sops.secrets.\u0026#34;k3s/token\u0026#34;.path; }) ]; }; } Extra Reading # NixOS Documentation\nDefining k3s in Pure Nix\n","externalUrl":null,"permalink":"/docs/kubernetes/01-introduction/","section":"Docs","summary":"","title":"Kubernetes: Introduction","type":"docs"},{"content":" Writing custom packages # Note We can use flake inputs for sources which don\u0026rsquo;t have versioned download links. Their hashes our updated by running the nix flake update command.\nFor software that is not included in the official Nix repository we can make use of Nix derivations to package our own applications: { src, lib, stdenv, fetchurl, autoPatchelfHook, gnutar, wrapGAppsHook3, glibc, gtk3, makeDesktopItem, }: let pname = \u0026#34;vuescan\u0026#34;; version = \u0026#34;9.8\u0026#34;; desktopItem = makeDesktopItem { name = \u0026#34;VueScan\u0026#34;; desktopName = \u0026#34;VueScan\u0026#34;; genericName = \u0026#34;Scanning Program\u0026#34;; comment = \u0026#34;Scanning Program\u0026#34;; icon = \u0026#34;vuescan\u0026#34;; terminal = false; type = \u0026#34;Application\u0026#34;; startupNotify = true; categories = [ \u0026#34;Graphics\u0026#34; \u0026#34;Utility\u0026#34; ]; keywords = [ \u0026#34;scan\u0026#34; \u0026#34;scanner\u0026#34; ]; exec = \u0026#34;vuescan\u0026#34;; }; in stdenv.mkDerivation rec { name = \u0026#34;${pname}-${version}\u0026#34;; inherit src; nativeBuildInputs = [ autoPatchelfHook gnutar wrapGAppsHook3 ]; buildInputs = [ glibc gtk3 ]; dontStrip = true; installPhase = \u0026#39;\u0026#39; install -m755 -D vuescan $out/bin/vuescan mkdir -p $out/share/icons/hicolor/scalable/apps/ mkdir -p $out/lib/udev/rules.d/ mkdir -p $out/share/applications/ cp vuescan.svg $out/share/icons/hicolor/scalable/apps/vuescan.svg cp vuescan.rul $out/lib/udev/rules.d/60-vuescan.rules ln -s ${desktopItem}/share/applications/* $out/share/applications runHook postInstall \u0026#39;\u0026#39;; meta = with lib; { homepage = \u0026#34;https://www.hamrick.com/about-vuescan.html\u0026#34;; description = \u0026#34;Scanning software for film scanners\u0026#34;; license = licenses.unfree; platforms = [ \u0026#34;x86_64-linux\u0026#34; ]; }; } Importing derivations # Custom package derivations can be imported into a nix configuration using overlays: flake.overlays.additional-packages = final: prev: { vuescan = final.callPackage ../../packages/vuescan.nix { src = inputs.vuescan-source; }; epson-v550-plugin = final.callPackage ../../packages/epson-v550-plugin.nix { }; cosmic-ext-applet-clipboard-manager = final.callPackage ../../packages/cosmic-ext-applet-clipboard-manager.nix { }; }; ","externalUrl":null,"permalink":"/docs/nixos/13-custom-packages/","section":"Docs","summary":"","title":"NixOS: Custom Packages","type":"docs"},{"content":" Generating Markdown documentation for NixOS modules # Using the nixosOptionsDoc package we can generate markdown documentation for both nixos and home-manager modules: perSystem = { self\u0026#39;, system, pkgs, ... }: { packages = { nixos-options-doc = let eval = inputs.nixpkgs.lib.evalModules { modules = [ { _module.check = false; } inputs.self.modules.nixos.core inputs.self.modules.nixos.desktop inputs.self.modules.nixos.server ]; }; cleanEval = inputs.nixpkgs.lib.filterAttrsRecursive (n: v: n != \u0026#34;_module\u0026#34;) eval; optionsDoc = pkgs.nixosOptionsDoc { inherit (cleanEval) options; }; in pkgs.runCommand \u0026#34;OPTIONS.md\u0026#34; { } \u0026#39;\u0026#39; cp ${optionsDoc.optionsCommonMark} $out \u0026#39;\u0026#39;; home-manager-options-doc = let eval = inputs.nixpkgs.lib.evalModules { modules = [ { _module.check = false; } inputs.self.modules.homeManager.secrets inputs.self.modules.homeManager.theme inputs.self.modules.homeManager.backup inputs.self.modules.homeManager.cosmic-manager inputs.self.modules.homeManager.development inputs.self.modules.homeManager.editing inputs.self.modules.homeManager.gaming inputs.self.modules.homeManager.plasma-manager inputs.self.modules.homeManager.utilities inputs.self.modules.homeManager.web ]; }; cleanEval = inputs.nixpkgs.lib.filterAttrsRecursive (n: v: n != \u0026#34;_module\u0026#34;) eval; optionsDoc = pkgs.nixosOptionsDoc { inherit (cleanEval) options; }; in pkgs.runCommand \u0026#34;OPTIONS.md\u0026#34; { } \u0026#39;\u0026#39; cp ${optionsDoc.optionsCommonMark} $out \u0026#39;\u0026#39;; }; }; ","externalUrl":null,"permalink":"/docs/nixos/12-generating-documentation/","section":"Docs","summary":"","title":"NixOS: Generating Module Documentation","type":"docs"},{"content":" Adding Nix checks # Using the pre-commit-hooks flake module we can define the set of checks to be run by the nix check command: { inputs, ... }: { perSystem = { self\u0026#39;, system, pkgs, ... }: { checks = { pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run { src = ../../.; hooks = { nixfmt-rfc-style.enable = true; flake-checker.enable = true; statix.enable = true; nil.enable = true; }; }; }; }; } Adding git hooks to run checks before committing # With the checks defined, we can create a nix dev-shell to install git hooks to run each check before commits: { inputs, ... }: { perSystem = { self\u0026#39;, system, pkgs, ... }: { devShells = { default = pkgs.mkShell { buildInputs = self\u0026#39;.checks.pre-commit-check.enabledPackages; shellHook = \u0026#39;\u0026#39; ${self\u0026#39;.checks.pre-commit-check.shellHook} exit \u0026#39;\u0026#39;; }; }; }; } ","externalUrl":null,"permalink":"/docs/nixos/11-git-hooks/","section":"Docs","summary":"","title":"NixOS: Ensuring Correctness with Git Hooks","type":"docs"},{"content":" Updating flake.lock using Github Actions # Using Determinate Systems nix-installer github action we can create a scheduled task to update the flake and merge the changes into our main branch: name: update-flake-lock on: workflow_dispatch: # allows manual triggering schedule: - cron: \u0026#39;0 0 * * 0\u0026#39; # runs weekly on Sunday at 00:00 jobs: lockfile: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Nix uses: DeterminateSystems/nix-installer-action@main - id: update name: Update flake.lock uses: DeterminateSystems/update-flake-lock@main with: pr-title: \u0026#34;Update flake.lock\u0026#34; # Title of PR to be created pr-labels: | # Labels to be set on the PR dependencies automated - name: Merge run: gh pr merge --auto \u0026#34;${{ steps.update.outputs.pull-request-number }}\u0026#34; --rebase env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} if: ${{ steps.update.outputs.pull-request-number != \u0026#39;\u0026#39; }} Updating system from remote repo # We can use the system.autoUpgrade configuration option to keep the system installation up to date with the main branch: { inputs, ... }: { flake.modules.nixos.auto-upgrade = { config, lib, pkgs, ... }: { options = { auto-upgrade.enable = lib.mkEnableOption \u0026#34;automatic update of nix flake from github\u0026#34;; }; config = lib.mkIf config.auto-upgrade.enable { system.autoUpgrade = { enable = true; flake = lib.mkDefault \u0026#34;github:robbiejennings/nix-config\u0026#34;; flags = lib.mkDefault [ \u0026#34;-L\u0026#34; # print build logs ]; dates = lib.mkDefault \u0026#34;02:00\u0026#34;; randomizedDelaySec = lib.mkDefault \u0026#34;45min\u0026#34;; }; }; }; } ","externalUrl":null,"permalink":"/docs/nixos/10-automatic-updates/","section":"Docs","summary":"","title":"NixOS: Automatic Updates","type":"docs"},{"content":" Installing Flatpak applications # Using the nix-flatpak home-manager module we can install Flatpak applications declaratively. This ensures that including applications are installed upon each rebuild: { inputs, ... }: { flake.modules.homeManager.firefox = { pkgs, lib, config, ... }: { options = { firefox.enable = lib.mkEnableOption \u0026#34;firefox web browser\u0026#34;; }; config = lib.mkIf config.firefox.enable { services.flatpak.packages = [ { appId = \u0026#34;org.mozilla.firefox\u0026#34;; origin = \u0026#34;flathub\u0026#34;; } ]; }; }; } ","externalUrl":null,"permalink":"/docs/nixos/09-flatpaks/","section":"Docs","summary":"","title":"NixOS: Installing Flatpaks","type":"docs"},{"content":" Backing up user files to Google Drive # Using a combination of Rclone and Restic we can create scheduled, deduplicated backups of user files. This requires a a valid client ID, client secret and access token set up through the google developer console.\nConfiguring Rclone # Rclone can be set up using a sops-nix template as such: { inputs, ... }: { flake.modules.homeManager.rclone = { pkgs, lib, config, ... }: { options = { rclone.enable = lib.mkEnableOption \u0026#34;rclone google drive remote\u0026#34;; secrets.rclone.enable = lib.mkEnableOption \u0026#34;rclone google drive secrets\u0026#34;; }; config = lib.mkMerge [ (lib.mkIf config.rclone.enable { programs.rclone.enable = true; }) (lib.mkIf (config.rclone.enable \u0026amp;\u0026amp; config.secrets.enable \u0026amp;\u0026amp; config.secrets.rclone.enable) { sops = { secrets = { \u0026#34;restic/repository\u0026#34; = { }; \u0026#34;restic/password\u0026#34; = { }; \u0026#34;rclone/client_id\u0026#34; = { }; \u0026#34;rclone/client_secret\u0026#34; = { }; \u0026#34;rclone/access_token\u0026#34; = { }; }; templates.\u0026#34;rclone.conf\u0026#34;.content = \u0026#39;\u0026#39; [gdrive] scope = drive.file type = drive client_id = ${config.sops.placeholder.\u0026#34;rclone/client_id\u0026#34;} client_secret = ${config.sops.placeholder.\u0026#34;rclone/client_secret\u0026#34;} token = ${config.sops.placeholder.\u0026#34;rclone/access_token\u0026#34;} \u0026#39;\u0026#39;; }; home.activation.\u0026#34;rclone.conf\u0026#34; = \u0026#39;\u0026#39; mkdir -p ~/.config/rclone ln -sf ${config.sops.templates.\u0026#34;rclone.conf\u0026#34;.path} ~/.config/rclone/rclone.conf \u0026#39;\u0026#39;; }) ]; }; } Configuring Restic # Restic can be configured using the services.restic config option as follows: { inputs, ... }: { flake.modules.homeManager.restic = { pkgs, lib, config, ... }: { options = { restic.enable = lib.mkEnableOption \u0026#34;restic backups\u0026#34;; secrets.restic.enable = lib.mkEnableOption \u0026#34;restic repository secrets\u0026#34;; }; config = lib.mkMerge [ (lib.mkIf config.restic.enable { home.packages = with pkgs; [ restic restic-browser ]; }) (lib.mkIf (config.restic.enable \u0026amp;\u0026amp; config.secrets.enable \u0026amp;\u0026amp; config.secrets.restic.enable) { sops.secrets = { \u0026#34;restic/repository\u0026#34; = { }; \u0026#34;restic/password\u0026#34; = { }; }; services.restic = { enable = true; backups.daily = { repositoryFile = config.sops.secrets.\u0026#34;restic/repository\u0026#34;.path; passwordFile = config.sops.secrets.\u0026#34;restic/password\u0026#34;.path; paths = [ \u0026#34;${config.home.homeDirectory}/Documents\u0026#34; \u0026#34;${config.home.homeDirectory}/Pictures\u0026#34; \u0026#34;${config.home.homeDirectory}/Books\u0026#34; ]; pruneOpts = [ \u0026#34;--keep-daily 3\u0026#34; \u0026#34;--keep-weekly 7\u0026#34; \u0026#34;--keep-monthly 15\u0026#34; \u0026#34;--keep-yearly 30\u0026#34; ]; timerConfig = { OnCalendar = \u0026#34;00:00\u0026#34;; RandomizedDelaySec = \u0026#34;45m\u0026#34;; Persistent = true; }; }; }; }) ]; }; } ","externalUrl":null,"permalink":"/docs/nixos/08-restic/","section":"Docs","summary":"","title":"NixOS: Backing Up with Restic","type":"docs"},{"content":" Adding options for colour schemes, fonts and wallpapers # Using Stylix, we can apply base16 colour schemes, wallpapers and fonts. Separate modules should be instantiated for nixos for system theming and home-manager for user theming. The options and contents for each of these modules can be identical: { inputs, ... }: let module = { config, lib, pkgs, ... }: { options = { theme = { enable = lib.mkEnableOption \u0026#34;stylix theme\u0026#34;; polarity = lib.mkOption { type = lib.types.enum [ \u0026#34;light\u0026#34; \u0026#34;dark\u0026#34; ]; default = \u0026#34;dark\u0026#34;; description = \u0026#34;light or dark theme\u0026#34;; }; base16Scheme = lib.mkOption { type = lib.types.str; default = \u0026#34;default-dark\u0026#34;; description = \u0026#34;tinted theming colour scheme\u0026#34;; example = \u0026#34;catppuccin-mocha\u0026#34;; }; image = lib.mkOption { type = lib.types.submodule { options = { url = lib.mkOption { type = lib.types.str; description = \u0026#34;Download URL\u0026#34;; }; hash = lib.mkOption { type = lib.types.str; description = \u0026#34;SHA256 hash in SRI format\u0026#34;; }; }; }; default = { url = \u0026#34;https://raw.githubusercontent.com/NixOS/nixos-artwork/refs/heads/master/wallpapers/nix-wallpaper-simple-dark-gray.png\u0026#34;; hash = \u0026#34;sha256-JaLHdBxwrphKVherDVe5fgh+3zqUtpcwuNbjwrBlAok=\u0026#34;; }; description = \u0026#34;Custom source with URL and hash\u0026#34;; }; fonts = lib.mkOption { type = lib.types.submodule { options = { interface = lib.mkOption { type = lib.types.submodule { options = { package = lib.mkOption { type = lib.types.package; description = \u0026#34;package to use for the interface font\u0026#34;; }; name = lib.mkOption { type = lib.types.str; description = \u0026#34;The name to use for the interface font\u0026#34;; }; }; }; default = { package = pkgs.inter; name = \u0026#34;Inter\u0026#34;; }; defaultText = lib.literalExpression \u0026#34;Inter\u0026#34;; description = \u0026#34;The font to use for the interface\u0026#34;; }; monospace = lib.mkOption { type = lib.types.submodule { options = { package = lib.mkOption { type = lib.types.package; description = \u0026#34;package to use for the monospace font\u0026#34;; }; name = lib.mkOption { type = lib.types.str; description = \u0026#34;The name to use for the monospace font\u0026#34;; }; }; }; default = { package = pkgs.nerd-fonts.jetbrains-mono; name = \u0026#34;JetBrainsMono Nerd Font Mono\u0026#34;; }; defaultText = lib.literalExpression \u0026#34;JetBrainsMono Nerd Font Mono\u0026#34;; description = \u0026#34;The font to use for the terminal\u0026#34;; }; emoji = lib.mkOption { type = lib.types.submodule { options = { package = lib.mkOption { type = lib.types.package; description = \u0026#34;package to use for the emoji font\u0026#34;; }; name = lib.mkOption { type = lib.types.str; description = \u0026#34;The name to use for the emoji font\u0026#34;; }; }; }; default = { package = pkgs.noto-fonts-color-emoji; name = \u0026#34;Noto Color Emoji\u0026#34;; }; defaultText = lib.literalExpression \u0026#34;Noto Color Emoji\u0026#34;; description = \u0026#34;The font to use for emoji\u0026#34;; }; }; }; default = { }; description = \u0026#34;Fonts used for interface, terminal and emojis\u0026#34;; }; }; }; config = lib.mkIf config.theme.enable { stylix = { enable = true; inherit (config.theme.polarity) ; base16Scheme = \u0026#34;${pkgs.base16-schemes}/share/themes/${config.theme.base16Scheme}.yaml\u0026#34;; image = pkgs.fetchurl config.theme.image; fonts = { serif = config.theme.fonts.interface; sansSerif = config.theme.fonts.interface; inherit (config.theme.fonts.monospace) ; inherit (config.theme.fonts.emoji) ; }; }; }; }; in { flake.modules.nixos.theme = module; flake.modules.homeManager.theme = module; } Setting theme options in our config # With the theme modules loaded we can configure themes for each system and user: theme = { image = { url = \u0026#34;https://raw.githubusercontent.com/AngelJumbo/gruvbox-wallpapers/refs/heads/main/wallpapers/photography/forest-2.jpg\u0026#34;; hash = \u0026#34;sha256-RqzCCnn4b5kU7EYgaPF19Gr9I5cZrkEdsTu+wGaaMFI=\u0026#34;; }; base16Scheme = \u0026#34;gruvbox-material-dark-hard\u0026#34;; }; ","externalUrl":null,"permalink":"/docs/nixos/07-stylix/","section":"Docs","summary":"","title":"NixOS: Theming with Stylix","type":"docs"},{"content":" Encrypting secrets # Secrets such as API keys and passwords should never be stored in plain text. Instead, we can use sops-nix to import this sensitive data from encrypted files. This negates the need to omit certain files from public repositories and reduces the risk of leaks due to human error.\nPrerequisites # Generating age key # In order to encrypt and decrypt secret files, we must first tell sops the location of our age file. This can be generated from an SSH key using the ssh-to-age package. In this case, we use separate keys for root and user secrets:\n# Generate user age mkdir -p ~/.config/sops/age ssh-to-age -private-key -i ~/.ssh/id_ed25519 \u0026gt; ~/.config/sops/age/keys.txt # Generate root age mkdir -p /root/.config/sops/age ssh-to-age -private-key -i /root/.ssh/id_ed25519 \u0026gt; /root/.config/sops/age/keys.txt Creating secret files # Once age keys are generated for both root and user secrets, we can use sops to generate secret files. For simple importing, we can reuse our username and hostname config options for these filenames:\n# Create user secrets sops edit ./secrets/\u0026lt;username\u0026gt;.yaml # Create root secrets sops edit ./secrets/\u0026lt;hostname\u0026gt;.yaml Importing secrets in our nix config # Before using secrets, we must add a configuration module to setup sops-nix for both nixos (root secrets) and home-manager (user-secrets): { inputs, ... }: { flake.modules.nixos.secrets = { config, lib, pkgs, ... }: { options = { secrets.enable = lib.mkEnableOption \u0026#34;importing secrets using sops-nix\u0026#34;; secrets.passwords.enable = lib.mkEnableOption \u0026#34;user password secrets\u0026#34;; }; config = lib.mkIf config.secrets.enable { environment.systemPackages = [ pkgs.sops pkgs.ssh-to-age ]; sops = { defaultSopsFile = lib.mkDefault ../../secrets/${config.networking.hostName}.yaml; age.sshKeyPaths = if config.impermanence.enable then lib.mkDefault [ \u0026#34;/persist/root/.ssh/id_ed25519\u0026#34; ] else lib.mkDefault [ \u0026#34;/root/.ssh/id_ed25519\u0026#34; ]; }; }; }; flake.modules.homeManager.secrets = { config, lib, pkgs, ... }: { options = { secrets.enable = lib.mkEnableOption \u0026#34;importing secrets using sops-nix\u0026#34;; }; config = lib.mkIf config.secrets.enable { sops = { defaultSopsFile = ../../secrets/${config.home.username}.yaml; age.sshKeyPaths = [ \u0026#34;/home/${config.home.username}/.ssh/id_ed25519\u0026#34; ]; }; }; }; } Then we can import root secrets such as user passwords like: (lib.mkIf (config.secrets.enable \u0026amp;\u0026amp; config.secrets.passwords.enable) { sops.secrets.\u0026#34;passwords/${username}\u0026#34;.neededForUsers = true; users.users.${username} = { initialPassword = null; hashedPasswordFile = config.sops.secrets.\u0026#34;passwords/${username}\u0026#34;.path; }; }) We can import user secrets such as license keys and create template files like: (lib.mkIf (config.vuescan.enable \u0026amp;\u0026amp; config.secrets.enable \u0026amp;\u0026amp; config.secrets.vuescan.enable) { sops = { secrets = { \u0026#34;vuescan/user_id\u0026#34; = { }; \u0026#34;vuescan/email_address\u0026#34; = { }; \u0026#34;vuescan/customer_number\u0026#34; = { }; \u0026#34;vuescan/serial_number\u0026#34; = { }; }; templates.\u0026#34;.vuescanrc\u0026#34;.content = \u0026#39;\u0026#39; UserID=${config.sops.placeholder.\u0026#34;vuescan/user_id\u0026#34;} SerialNumber=${config.sops.placeholder.\u0026#34;vuescan/serial_number\u0026#34;} CustomerNumber=${config.sops.placeholder.\u0026#34;vuescan/customer_number\u0026#34;} EmailAddress=${config.sops.placeholder.\u0026#34;vuescan/email_address\u0026#34;} \u0026#39;\u0026#39;; }; home.activation.\u0026#34;vuescanrc\u0026#34; = \u0026#39;\u0026#39; ln -sf ${config.sops.templates.\u0026#34;.vuescanrc\u0026#34;.path} ~/.vuescanrc \u0026#39;\u0026#39;; }) ","externalUrl":null,"permalink":"/docs/nixos/06-sops-nix/","section":"Docs","summary":"","title":"NixOS: Encrypting Secrets with SOPS","type":"docs"},{"content":" Importing the Impermanence module # As we use Nix to define our system state, the only directories actually required for boot are /boot and /nix. We can leverage this to ensure only desired files are actually retained on boot, eliminating cruft and ensuring pristine state upon each reboot. This can be done declaratively by importing the Impermanence module flake.\nDestroying root # Using our BTRFS volume defined in the previous section, we can add an initrd service to delete the entire /root sub-partition on boot. Nix will then regenerate all required files with symlinks to the nix store located in the separate /nix sub-partition. { inputs, ... }: { flake.modules.nixos.impermanence = { config, lib, pkgs, ... }: { options = { impermanence.enable = lib.mkEnableOption \u0026#34;impermanence\u0026#34;; }; config = lib.mkIf config.impermanence.enable { fileSystems.\u0026#34;/persist\u0026#34;.neededForBoot = true; boot.initrd.systemd.services.rollback = { description = \u0026#34;Rollback BTRFS root subvolume to a pristine state\u0026#34;; wantedBy = [ \u0026#34;initrd.target\u0026#34; ]; after = [ \u0026#34;initrd-root-device.target\u0026#34; ]; before = [ \u0026#34;sysroot.mount\u0026#34; ]; unitConfig.DefaultDependencies = \u0026#34;no\u0026#34;; serviceConfig.Type = \u0026#34;oneshot\u0026#34;; script = \u0026#39;\u0026#39; set -e mkdir -p /mnt mount ${config.fileSystems.\u0026#34;/\u0026#34;.device} /mnt if [[ -e /mnt/root ]]; then btrfs subvolume delete -R /mnt/root fi btrfs subvolume create /mnt/root umount /mnt \u0026#39;\u0026#39;; }; }; }; } Persisting files # Using the environment.persistence.\u0026lt;persistence sub-partition\u0026gt; configuration option we can set desired system directories such as bluetooth and network settings to be mounted to the persisted sub-partition and symlinked to their actual location in the root sub-partition: impermanence.enable = true; environment.persistence.\u0026#34;/persist\u0026#34; = { hideMounts = true; directories = [ \u0026#34;/var/log\u0026#34; \u0026#34;/var/lib/bluetooth\u0026#34; \u0026#34;/var/lib/nixos\u0026#34; \u0026#34;/var/lib/systemd/coredump\u0026#34; \u0026#34;/var/lib/libvirt\u0026#34; \u0026#34;/var/lib/netbird\u0026#34; \u0026#34;/etc/NetworkManager/system-connections\u0026#34; \u0026#34;/etc/nixos\u0026#34; \u0026#34;/root/.ssh\u0026#34; ]; }; Similarly, we can persist user files such as Desktop, Documents and Flatpak applications: (lib.mkIf config.impermanence.enable { environment.persistence.\u0026#34;/persist\u0026#34;.users.${username} = { directories = [ \u0026#34;Desktop\u0026#34; \u0026#34;Documents\u0026#34; \u0026#34;Downloads\u0026#34; \u0026#34;Music\u0026#34; \u0026#34;Pictures\u0026#34; \u0026#34;Videos\u0026#34; \u0026#34;Games\u0026#34; \u0026#34;Books\u0026#34; \u0026#34;nix-config\u0026#34; { directory = \u0026#34;.ssh\u0026#34;; mode = \u0026#34;0700\u0026#34;; } { directory = \u0026#34;.local/share/keyrings\u0026#34;; mode = \u0026#34;0700\u0026#34;; } { directory = \u0026#34;.local/share/kwalletd\u0026#34;; mode = \u0026#34;0700\u0026#34;; } \u0026#34;.local/share/flatpak\u0026#34; \u0026#34;.local/share/Steam\u0026#34; \u0026#34;.local/share/PrismLauncher\u0026#34; \u0026#34;.local/state/cosmic\u0026#34; \u0026#34;.local/state/cosmic-comp\u0026#34; \u0026#34;.config\u0026#34; \u0026#34;.var\u0026#34; \u0026#34;.vscode-oss/extensions\u0026#34; ]; files = [ \u0026#34;.bash_history\u0026#34; \u0026#34;.zsh_history\u0026#34; ]; }; }) ","externalUrl":null,"permalink":"/docs/nixos/05-impermanence/","section":"Docs","summary":"","title":"NixOS: Ephemeral Root with Impermanence","type":"docs"},{"content":" Importing the Disko module # Before installing NixOS, it is important that all drives are correctly formatted and mounted. This can be done declaratively by importing the Disko module flake.\nDefining disk partitions # Disk partitions can be defined using the disko.devices configuration option. An example of a BTRFS boot drive declaration including LUKS encryption and dedicated persistence sub-partition looks like: { inputs, ... }: { flake.modules.nixos.xps15-disk = { config, lib, pkgs, ... }: { disko.devices = { disk = { main = { type = \u0026#34;disk\u0026#34;; device = \u0026#34;/dev/nvme0n1\u0026#34;; content = { type = \u0026#34;gpt\u0026#34;; partitions = { boot = { size = \u0026#34;1M\u0026#34;; type = \u0026#34;EF02\u0026#34;; # for grub MBR }; ESP = { size = \u0026#34;512M\u0026#34;; type = \u0026#34;EF00\u0026#34;; content = { type = \u0026#34;filesystem\u0026#34;; format = \u0026#34;vfat\u0026#34;; mountpoint = \u0026#34;/boot\u0026#34;; }; }; luks = { size = \u0026#34;100%\u0026#34;; content = { type = \u0026#34;luks\u0026#34;; name = \u0026#34;crypted\u0026#34;; content = { type = \u0026#34;btrfs\u0026#34;; extraArgs = [ \u0026#34;-f\u0026#34; ]; subvolumes = { \u0026#34;/root\u0026#34; = { mountpoint = \u0026#34;/\u0026#34;; }; \u0026#34;/persist\u0026#34; = { mountpoint = \u0026#34;/persist\u0026#34;; mountOptions = [ \u0026#34;subvol=persist\u0026#34; ]; }; \u0026#34;/nix\u0026#34; = { mountpoint = \u0026#34;/nix\u0026#34;; mountOptions = [ \u0026#34;subvol=nix\u0026#34; ]; }; \u0026#34;/swap\u0026#34; = { mountpoint = \u0026#34;/.swapvol\u0026#34;; swap.swapfile.size = \u0026#34;16384M\u0026#34;; }; }; }; }; }; }; }; }; }; }; }; } Partitioning disks # Prior to installing NixOS, we can partition disks like:\nsudo nix run --experimental-features \u0026#34;nix-command flakes\u0026#34; github:nix-community/disko/latest -- --mode destroy,format,mount --flake github:robbiejennings/nix-config#\u0026lt;system\u0026gt; ","externalUrl":null,"permalink":"/docs/nixos/04-disko/","section":"Docs","summary":"","title":"NixOS: Partitioning Drives with Disko","type":"docs"},{"content":" Introduction # The Dendritic Pattern is a simple, yet powerful approach to defining complex Nix projects containing a mix of nixos and home-manager modules without the need for spaghetti code or complex wiring functions. This is accomplished using the Flake Parts module to define every aspect as a top level function, all of which may be merged at evaluation time. Each module can be imported at the flake level using the handy Import-Tree module.\nDefining a dendritic flake # The flake.nix of a dendritic nixos configuration should contain only three elements:\nA description of the flake A list of imports for the flake A single output making use of the flake parts mkFlake function A simple flake to create a nixos-configuration with modules defined in the modules subdirectory looks like:\n{ description = \u0026#34;Robbie\u0026#39;s NixOS flake\u0026#34;; inputs = { nixpkgs = { url = \u0026#34;github:NixOS/nixpkgs/nixos-25.11\u0026#34;; }; }; outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules); } Defining a module for basic settings # A single module can be defined for re-used nix and home-manager settings to reduce code duplication across configurations: { self, inputs, ... }: { flake.modules.nixos.settings = { config, lib, pkgs, ... }: { imports = [ inputs.home-manager.nixosModules.home-manager inputs.impermanence.nixosModules.impermanence inputs.sops-nix.nixosModules.sops inputs.disko.nixosModules.disko inputs.stylix.nixosModules.stylix ]; nix.settings = { auto-optimise-store = true; experimental-features = [ \u0026#34;nix-command\u0026#34; \u0026#34;flakes\u0026#34; ]; }; nixpkgs = { config.allowUnfree = true; overlays = [ self.overlays.unstable-packages self.overlays.additional-packages ]; }; home-manager = { useGlobalPkgs = true; useUserPackages = true; backupFileExtension = \u0026#34;backup\u0026#34;; sharedModules = [ { imports = [ inputs.sops-nix.homeManagerModules.sops inputs.stylix.homeModules.stylix ]; home.stateVersion = \u0026#34;25.11\u0026#34;; } ]; }; stylix.homeManagerIntegration.autoImport = false; users.mutableUsers = false; system.stateVersion = \u0026#34;25.11\u0026#34;; }; } Defining a feature module # Though modules are composable, each module file should handle a single repsonsibility. For example, a desktop module may contain many individual feature modules to cover the desktop environments, audio interfaces, etc\u0026hellip;\nSingle feature # An \u0026ldquo;audio\u0026rdquo; module may look like: { inputs, ... }: { flake.modules.nixos.audio = { config, lib, pkgs, ... }: { options = { audio.enable = lib.mkEnableOption \u0026#34;audio using pipewire\u0026#34;; }; config = lib.mkIf config.audio.enable { services.pipewire = { enable = true; pulse.enable = lib.mkDefault true; }; }; }; } Multi-feature # This may be imported into a high-level \u0026ldquo;desktop\u0026rdquo; module like: { inputs, ... }: { flake.modules.nixos.desktop = { config, lib, pkgs, ... }: { imports = [ inputs.self.modules.nixos.audio inputs.self.modules.nixos.bluetooth inputs.self.modules.nixos.cosmic-desktop inputs.self.modules.nixos.kde-plasma inputs.self.modules.nixos.kde-connect inputs.self.modules.nixos.printing inputs.self.modules.nixos.scanning inputs.self.modules.nixos.steam inputs.self.modules.nixos.virtualisation inputs.self.modules.nixos.qmk ]; options = { desktopEnvironment = lib.mkOption { type = lib.types.enum [ \u0026#34;plasma\u0026#34; \u0026#34;cosmic\u0026#34; ]; default = \u0026#34;plasma\u0026#34;; description = \u0026#34;Select desktop environment: Plasma or COSMIC.\u0026#34;; }; }; config = { services.flatpak.enable = true; bootloader.pretty = lib.mkDefault true; audio.enable = lib.mkDefault true; bluetooth.enable = lib.mkDefault true; cosmic-desktop.enable = if config.desktopEnvironment == \u0026#34;cosmic\u0026#34; then true else false; kde-plasma.enable = if config.desktopEnvironment == \u0026#34;plasma\u0026#34; then true else false; kde-connect.enable = lib.mkDefault true; printing.enable = lib.mkDefault true; scanning.enable = lib.mkDefault true; steam.enable = lib.mkDefault true; virtualisation.enable = lib.mkDefault true; qmk.enable = lib.mkDefault true; home-manager.sharedModules = [ { imports = [ inputs.nix-flatpak.homeManagerModules.nix-flatpak inputs.plasma-manager.homeModules.plasma-manager inputs.cosmic-manager.homeManagerModules.cosmic-manager ]; } ]; assertions = [ { assertion = !(config.cosmic-desktop.enable \u0026amp;\u0026amp; config.kde-plasma.enable); message = \u0026#34;Cannot enable both COSMIC and Plasma at the same time.\u0026#34;; } ]; }; }; } The factory method # Adding a named \u0026ldquo;factory\u0026rdquo; flake module with an unspecified attribute list as its type will allow for the creation of factory modules. These modules look similar to typical nixos or home-manager modules with the exception that they take an addtional attribute set as the initial argument and can then be re-used to instantiate multiple modules using the same logic.\nThe factory module should look like: { lib, ... }: { options.flake.factory = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; default = { }; }; } This flake module can then be used to instantiate a desktop-user nixos module like: { self, inputs, ... }: { config.flake.factory.desktop-user = { username, isAdmin, }: { config, lib, pkgs, ... }: { config = lib.mkMerge [ { users.users.\u0026#34;${username}\u0026#34; = { initialPassword = lib.mkDefault \u0026#34;password\u0026#34;; isNormalUser = true; home = \u0026#34;/home/${username}\u0026#34;; extraGroups = [ \u0026#34;networkmanager\u0026#34; \u0026#34;docker\u0026#34; \u0026#34;libvirtd\u0026#34; ] ++ lib.optionals isAdmin [ \u0026#34;wheel\u0026#34; ]; }; home-manager.users.\u0026#34;${username}\u0026#34; = { imports = [ inputs.self.modules.homeManager.development inputs.self.modules.homeManager.utilities inputs.self.modules.homeManager.web inputs.self.modules.homeManager.gaming inputs.self.modules.homeManager.editing inputs.self.modules.homeManager.backup ]; }; } (lib.mkIf (config.secrets.enable \u0026amp;\u0026amp; config.secrets.passwords.enable) { sops.secrets.\u0026#34;passwords/${username}\u0026#34;.neededForUsers = true; users.users.${username} = { initialPassword = null; hashedPasswordFile = config.sops.secrets.\u0026#34;passwords/${username}\u0026#34;.path; }; }) (lib.mkIf config.impermanence.enable { environment.persistence.\u0026#34;/persist\u0026#34;.users.${username} = { directories = [ \u0026#34;Desktop\u0026#34; \u0026#34;Documents\u0026#34; \u0026#34;Downloads\u0026#34; \u0026#34;Music\u0026#34; \u0026#34;Pictures\u0026#34; \u0026#34;Videos\u0026#34; \u0026#34;Games\u0026#34; \u0026#34;Books\u0026#34; \u0026#34;nix-config\u0026#34; { directory = \u0026#34;.ssh\u0026#34;; mode = \u0026#34;0700\u0026#34;; } { directory = \u0026#34;.local/share/keyrings\u0026#34;; mode = \u0026#34;0700\u0026#34;; } { directory = \u0026#34;.local/share/kwalletd\u0026#34;; mode = \u0026#34;0700\u0026#34;; } \u0026#34;.local/share/flatpak\u0026#34; \u0026#34;.local/share/Steam\u0026#34; \u0026#34;.local/share/PrismLauncher\u0026#34; \u0026#34;.local/state/cosmic\u0026#34; \u0026#34;.local/state/cosmic-comp\u0026#34; \u0026#34;.config\u0026#34; \u0026#34;.var\u0026#34; \u0026#34;.vscode-oss/extensions\u0026#34; ]; files = [ \u0026#34;.bash_history\u0026#34; \u0026#34;.zsh_history\u0026#34; ]; }; }) ]; }; } Creating a system configuration # Bringing everything together, we can create a complete NixOS system configuration using the flake.nixosConfigurations.\u0026lt;hostname\u0026gt; function. A laptop system module may look like: { self, inputs, lib, ... }: { flake.modules.nixos.robbie-laptop = lib.mkMerge [ (self.factory.desktop-user { username = \u0026#34;robbie\u0026#34;; isAdmin = true; }) { home-manager.users.robbie = { programs.git = { enable = true; settings.user = { name = \u0026#34;robbiejennings\u0026#34;; email = \u0026#34;robbie.jennings97@gmail.com\u0026#34;; }; }; theme = { image = { url = \u0026#34;https://raw.githubusercontent.com/AngelJumbo/gruvbox-wallpapers/refs/heads/main/wallpapers/photography/forest-2.jpg\u0026#34;; hash = \u0026#34;sha256-RqzCCnn4b5kU7EYgaPF19Gr9I5cZrkEdsTu+wGaaMFI=\u0026#34;; }; base16Scheme = \u0026#34;gruvbox-material-dark-hard\u0026#34;; }; secrets = { enable = true; vuescan.enable = true; rclone.enable = true; restic.enable = true; }; }; } ]; flake.nixosConfigurations.xps15 = inputs.nixpkgs.lib.nixosSystem { modules = [ inputs.self.modules.nixos.settings inputs.self.modules.nixos.xps15 inputs.self.modules.nixos.core inputs.self.modules.nixos.desktop inputs.self.modules.nixos.robbie-laptop { networking.hostName = \u0026#34;xps15\u0026#34;; desktopEnvironment = \u0026#34;cosmic\u0026#34;; secrets = { enable = true; passwords.enable = true; }; impermanence.enable = true; environment.persistence.\u0026#34;/persist\u0026#34; = { hideMounts = true; directories = [ \u0026#34;/var/log\u0026#34; \u0026#34;/var/lib/bluetooth\u0026#34; \u0026#34;/var/lib/nixos\u0026#34; \u0026#34;/var/lib/systemd/coredump\u0026#34; \u0026#34;/var/lib/libvirt\u0026#34; \u0026#34;/var/lib/netbird\u0026#34; \u0026#34;/etc/NetworkManager/system-connections\u0026#34; \u0026#34;/etc/nixos\u0026#34; \u0026#34;/root/.ssh\u0026#34; ]; }; } ]; }; } ","externalUrl":null,"permalink":"/docs/nixos/03-dendritic-pattern/","section":"Docs","summary":"","title":"NixOS: The Dendritic Pattern","type":"docs"},{"content":" Installation # Provision Disks using Disko # Disks must first be prepared for installation. Thankfully, this can be fully automated in Nix using the handy Disko tool. Be careful to take full backups as all data on the host will be erased upon formatting. If installing a system with LUKS encryption, you will be prompted to add a secure password.\n# Provision disks sudo nix run --experimental-features \u0026#34;nix-command flakes\u0026#34; github:nix-community/disko/latest -- --mode destroy,format,mount --flake github:robbiejennings/nix-config#\u0026lt;system\u0026gt; Install a NixOS system # Once Disko is finished formatting disks and initialising the required filesystems we can move onto the NixOS installation. Again, this is a simple one-line command where you will be asked for a secure root password.\n# Install NixOS sudo nixos-install --flake github:robbiejennings/nix-config#\u0026lt;system\u0026gt; ","externalUrl":null,"permalink":"/docs/nixos/02-installation/","section":"Docs","summary":"","title":"NixOS: Installation","type":"docs"},{"content":" Introduction # This documentation covers my NixOS configuration used to deploy my desktop and homelab systems. Specifically, the patterns adopted and unique features are described with code examples.\nPrerequisites # This documentation assumes understanding of Linux, NixOS and the Nix language. Before starting, it is recommended to familiarise yourself with the core concepts behind these technologies.\nExtra Reading # My Nix Config\nDendritic Pattern\nDendritic Pattern - Extra Documentation\n","externalUrl":null,"permalink":"/docs/nixos/01-introduction/","section":"Docs","summary":"","title":"NixOS: Introduction","type":"docs"},{"content":"By far the biggest limiting factor to setting up a darkroom at home is space. This is why I set out to make the most out of Ilford\u0026rsquo;s pop-up darkroom tent to make the smallest darkroom capable of making large (16\u0026quot;x20\u0026quot;) prints possible.\nA 60\u0026quot;x120\u0026quot; tabletop with cheap workbench and DIY sliding shelves made from ikea wardrobe furniture make up the basic structure. On top sits a Durst M605 colour enlarger, AP safelight, Patterson timer and focus finder and Beard adjustable masking easel. A pair of flexible lamps allow for easy inspection of developed prints without the need to open the lightproof tent.\nThe next tier consists of a sliding \u0026ldquo;wet section\u0026rdquo; which contains three 8\u0026quot;x10\u0026quot; development trays colour coded for development (red), stop bath (white) and fixer (grey). A fourth tray used for washing test prints sits above the fixer tray for easy transfer with minimal risk for cross contamination. Larger prints can be made using the JOBO 2850 processing drum and a set of rollers.\nThe third tier is a shelf for additional printing papers and easels of various sizes along with a Patterson contact printer. Below is a larger shelf for drums, measuring cylinders and chemistry storage along with an ikea storage box for accessories such as a squeegees, scissors, gloves and film hanging clips.\nAll of this fits snugly in my garden shed taking up less than 1.5sqm of floor space and without the need for blacking out the whole room!\n","date":"11 May 2026","externalUrl":null,"permalink":"/posts/darkroom/","section":"Posts","summary":"","title":"Building my Darkroom Shed","type":"posts"},{"content":"","date":"11 May 2026","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"11 May 2026","externalUrl":null,"permalink":"/tags/darkroom/","section":"Tags","summary":"","title":"Darkroom","type":"tags"},{"content":"","date":"11 May 2026","externalUrl":null,"permalink":"/categories/photography/","section":"Categories","summary":"","title":"Photography","type":"categories"},{"content":"","date":"11 May 2026","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"11 May 2026","externalUrl":null,"permalink":"/","section":"Robbie Jennings","summary":"","title":"Robbie Jennings","type":"page"},{"content":"","date":"11 May 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"In December of 2024 I made my first ever trip to the US to visit my girlfriend\u0026rsquo;s parents and see the city of Houston where she grew up. As part of this trip, we ventured out for a three-day trip to New Orleans, the birthplace of jazz and the city of Voodoo.\nThe day prior Nola was hit with tragedy as a terrorist attack took the lives of fifteen innocent people celebrating the new year. I remember a sense of unease and serious questioning as to whether or not it would be safe for us to go that morning. Upon our arrival however I was touched by the tenacity and solidarity shown by the people of the Big Easy.\nWreaths and memorials were laid out at the site of the incident, not five minutes from my hotel. What was more striking though was the upbeat music that was played, the artists exhibiting their fine works, the poets clacking away at typewriters on folding tables. People here were deeply hurt by the tragedy, but they were in no way going to let it divide them or dim the rich culture that lights up their streets.\nLive bands were at every corner to treat the ears to jazz, blues and more than a few renditions of \u0026ldquo;House of the Rising Sun\u0026rdquo;. Without question this was the most musical place I had ever been and I loved every second of it. A quiet night at a jazz bar with drinks and songs dispersed with light-hearted stories told by the biggest piano man I had ever seen topped the experience off perfectly.\nMy girlfriend and her mother both got Voodoo readings while I had my first ever po\u0026rsquo;boy sandwich. I don\u0026rsquo;t know about the whole fortune-telling thing but the food in Louisiana is enough to want to come back for more.\n","date":"9 May 2026","externalUrl":null,"permalink":"/posts/new-orleans/","section":"Posts","summary":"","title":"My Trip to New Orleans","type":"posts"},{"content":"In November 2024 I visited Vienna for a weekend getaway as well as to see my girlfriend\u0026rsquo;s favourite band \u0026ldquo;Cigarettes After Sex\u0026rdquo; perform. An incredibly picturesque city, I couldn\u0026rsquo;t help but feel they had things just that little bit more figured out than the rest of us. Little noise, clean streets and pretty and affordable housing make for quite the calming environment in contrast to Dublin town.\nArt galleries and music halls are the most notorious attractions and though I didn\u0026rsquo;t get to hear any Mozart, I did get to see a limited-time exhibition of the works of Rembrandt. I am by no means an art aficionado but wow, did those paintings pop out of the frame. Honestly, I was surprised by just how much I enjoyed my day there.\nUnfortunately, we were just a tad too early to see the Christmas markets but at least we have a good excuse to return. I fancy another pint of Stiegl.\n","date":"8 May 2026","externalUrl":null,"permalink":"/posts/vienna/","section":"Posts","summary":"","title":"My Trip to Vienna","type":"posts"},{"content":" Phoenix Park Deer Bray Head Tree Bray Head Trunk Bray Head Moon Charlie Headshot Charlie Leaf Millie Papal Cross Seafront Kiosk New Orleans Vienna ","date":"1 January 2000","externalUrl":null,"permalink":"/gallery/","section":"Robbie Jennings","summary":"","title":"Gallery","type":"page"},{"content":"Welcome to my blog!\nA space for me to post my photography, document my code and share my travels and thoughts. I run an old-school black \u0026amp; white darkroom in my garden shed and a new-school Kubernetes and Nixos based homelab in my office.\nPreviously, I have worked professionally as a backend engineer at Murex and Aerlytix with experience in building responsive APIs for modern web services as well as database management.\nMy camera collection includes a Nikon FE, Rolleiflex Automat and Bronica SQ-A. My darkroom consists of a Durst M605 Colour enlarger and Ilford pop-up darkroom tent. I also digitise my prints and negatives using an Epson v550 flatbed scanner and Vuescan software.\nMy homelab serves all kinds of useful applications including Nextcloud, Forgejo, Immich, Jellyfin and the suite of Servarr applications. All of this accessible anywhere in the world using my Netbird mesh network.\nIf any of this sounds interesting to you then you\u0026rsquo;re in the right place! I hope you enjoy my projects and stories.\n","externalUrl":null,"permalink":"/about/","section":"Robbie Jennings","summary":"","title":"About Me","type":"page"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/development/","section":"Categories","summary":"","title":"Development","type":"categories"},{"content":"","externalUrl":null,"permalink":"/docs/","section":"Docs","summary":"","title":"Docs","type":"docs"},{"content":"","externalUrl":null,"permalink":"/categories/homelab/","section":"Categories","summary":"","title":"Homelab","type":"categories"},{"content":"","externalUrl":null,"permalink":"/series/kubernetes/","section":"Series","summary":"","title":"Kubernetes","type":"series"},{"content":"","externalUrl":null,"permalink":"/tags/kubernetes/","section":"Tags","summary":"","title":"Kubernetes","type":"tags"},{"content":"","externalUrl":null,"permalink":"/series/nixos/","section":"Series","summary":"","title":"NixOS","type":"series"},{"content":"","externalUrl":null,"permalink":"/tags/nixos/","section":"Tags","summary":"","title":"NixOS","type":"tags"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]