From 3a5b786b36f23f9bc70015d52399e3b261b2f266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Billet?= Date: Wed, 3 Dec 2025 22:36:07 +0100 Subject: [PATCH] Added Immich. Tweaked docker compose networks (frontend/backend). --- config-files/sops-nix/secrets.yaml | 12 +++ configuration.nix | 3 +- deploy.sh | 25 ++++-- docker/frigate.original | 4 + docker/hass.original | 7 +- docker/immich.original | 111 ++++++++++++++++++++++++++ docker/passbolt.original | 35 ++++---- docker/traefik.original | 21 +++++ ensure-pcr.nix | 123 +++++++++++++++++++++++++++++ 9 files changed, 309 insertions(+), 32 deletions(-) create mode 100644 docker/immich.original create mode 100644 ensure-pcr.nix diff --git a/config-files/sops-nix/secrets.yaml b/config-files/sops-nix/secrets.yaml index 20bcb72..d3c3098 100644 --- a/config-files/sops-nix/secrets.yaml +++ b/config-files/sops-nix/secrets.yaml @@ -33,6 +33,18 @@ docker: HOME_ROUTER_IP=$HOME_ROUTER_IP HOME_SERVER_IP=$HOME_SERVER_IP FTLCONF_webserver_api_password=$FTLCONF_WEBSERVER_PASSWORD + immich: | + DOMAIN_NAME=$DOMAIN_NAME + TZ=Europe/Paris + UPLOAD_LOCATION=/mnt/data-storage/docker-data/immich + IMMICH_VERSION=release + IMMICH_TRUSTED_PROXIES=172.16.50.253 + REDIS_HOSTNAME=immich-redis + DB_HOSTNAME=immich-database + DB_DATABASE_NAME=$IMMICH_DB_DATABASE_NAME + DB_USERNAME=$IMMICH_DB_USERNAME + DB_PASSWORD=$IMMICH_DB_PASSWORD + DB_DATA_LOCATION=/mnt/config-storage/docker-data/immich/database disks: data_disk_1: $DATA_DISK_1 diff --git a/configuration.nix b/configuration.nix index 2f9f965..cb7d595 100644 --- a/configuration.nix +++ b/configuration.nix @@ -43,6 +43,7 @@ in sops.secrets."docker/passbolt" = { owner = "numbus-admin"; path = "/etc/docker-compose/passbolt/.env"; }; sops.secrets."docker/hass" = { owner = "numbus-admin"; path = "/etc/docker-compose/hass/.env"; }; sops.secrets."docker/pihole" = { owner = "numbus-admin"; path = "/etc/docker-compose/pihole/.env"; }; + sops.secrets."docker/immich" = { owner = "numbus-admin"; path = "/etc/docker-compose/immich/.env"; }; sops.secrets."disks/data_disk_1" = { owner = "root"; }; sops.secrets."disks/data_disk_2" = { owner = "root"; }; sops.secrets."disks/data_disk_3" = { owner = "root"; }; @@ -125,7 +126,7 @@ in # Enable docker virtualisation.docker.enable = true; virtualisation.docker.daemon.settings = { - data-root = "/mnt/config-storage/docker-data/docker-volumes/"; + data-root = "/mnt/config-storage/docker-volumes/"; }; # Enable networking and firewall diff --git a/deploy.sh b/deploy.sh index ce53cea..75e92f1 100755 --- a/deploy.sh +++ b/deploy.sh @@ -171,6 +171,9 @@ files_generation() { export PASSBOLT_MYSQL_USER="$(openssl rand -hex 10)" export PASSBOLT_MYSQL_PASSWORD="$(openssl rand -base64 32 | tr -d '\=+/')" export FTLCONF_WEBSERVER_PASSWORD="$(openssl rand -base64 32 | tr -d '\=+/')" + export IMMICH_DB_DATABASE_NAME="$(openssl rand -hex 10)" + export IMMICH_DB_USERNAME="$(openssl rand -hex 10)" + export IMMICH_DB_PASSWORD="$(openssl rand -base64 32 | tr -d '\=+/')" export DATA_DISK_1="$(openssl rand -base64 32 | tr -d '\=+/')" export DATA_DISK_2="$(openssl rand -base64 32 | tr -d '\=+/')" export DATA_DISK_3="$(openssl rand -base64 32 | tr -d '\=+/')" @@ -217,21 +220,33 @@ files_generation() { sed -i s+HOME_ROUTER_IP+$HOME_ROUTER_IP+g configuration.nix echo -e "\n ✅ Adapting the docker configuration to your hardware..." - DEVICES_BLOCK="" + FRIGATE_DEVICES_BLOCK="" if [[ "$TARGET_GRAPHICS_RENDERER" == "true" ]]; then - DEVICES_BLOCK+=" - /dev/dri/renderD128:/dev/dri/renderD128\n" + FRIGATE_DEVICES_BLOCK+=" - /dev/dri:/dev/dri\n" fi if [[ "$TARGET_USB_CORAL" == "true" ]]; then - DEVICES_BLOCK+=" - /dev/bus/usb:/dev/bus/usb\n" + FRIGATE_DEVICES_BLOCK+=" - /dev/bus/usb:/dev/bus/usb\n" fi - if [[ -n "$DEVICES_BLOCK" ]]; then - REPLACEMENT="devices:\n${DEVICES_BLOCK%\\n}" + if [[ -n "$FRIGATE_DEVICES_BLOCK" ]]; then + REPLACEMENT="devices:\n${FRIGATE_DEVICES_BLOCK%\\n}" sed -i.bak "s|# --- frigate devices --- #|$REPLACEMENT|" docker/frigate.original else sed -i.bak "/# --- frigate devices --- #/d" docker/frigate.original fi + IMMICH_DEVICES_BLOCK="" + if [[ "$TARGET_GRAPHICS_RENDERER" == "true" ]]; then + IMMICH_DEVICES_BLOCK+=" - /dev/dri:/dev/dri\n" + fi + + if [[ -n "$IMMICH_DEVICES_BLOCK" ]]; then + REPLACEMENT="devices:\n${IMMICH_DEVICES_BLOCK%\\n}" + sed -i.bak "s|# --- immich devices --- #|$REPLACEMENT|" docker/immich.original + else + sed -i.bak "/# --- immich devices --- #/d" docker/immich.original + fi + if [[ -n "$TARGET_ZIGBEE_DEVICE" ]]; then REPLACEMENT="devices:\n - /dev/serial/by-id/${TARGET_ZIGBEE_DEVICE}:/dev/ttyUSB0" sed -i.bak "s|# --- hass devices --- #|$REPLACEMENT|" docker/hass.original diff --git a/docker/frigate.original b/docker/frigate.original index ccbfc90..2aa8c47 100644 --- a/docker/frigate.original +++ b/docker/frigate.original @@ -38,6 +38,10 @@ in # --- frigate devices --- # labels: - traefik.enable=true + - traefik.http.services.frigate.loadbalancer.server.port=8971 + - traefik.http.services.frigate.loadbalancer.server.scheme=http + - traefik.http.routers.frigate-https.entrypoints=websecure + - traefik.http.routers.frigate-https.rule=Host(`cctv.$DOMAIN_NAME`) - traefik.http.routers.frigate-https.tls=true - traefik.http.routers.frigate-https.tls.certresolver=cloudflare restart: unless-stopped diff --git a/docker/hass.original b/docker/hass.original index 2ab7bab..c848ed6 100644 --- a/docker/hass.original +++ b/docker/hass.original @@ -47,12 +47,7 @@ in networks: hass_backend: - name: hass_backend - driver: bridge - ipam: - config: - - subnet: "172.16.4.0/24" - gateway: "172.16.4.254" + external: true hass_frontend: external: true ''; diff --git a/docker/immich.original b/docker/immich.original new file mode 100644 index 0000000..0736b6c --- /dev/null +++ b/docker/immich.original @@ -0,0 +1,111 @@ +{ config, pkgs, ... }: + +let + container_name = "immich"; + compose-dir = "docker-compose/immich"; + config-dir = "/mnt/config-storage/docker-data/immich"; +in + +{ + config = { + environment.etc."${compose-dir}/compose.yaml".text = + /* + yaml + */ + '' + services: + immich-server: + image: ghcr.io/immich-app/immich-server:$IMMICH_VERSION + container_name: immich-server + networks: + immich_frontend: + immich_backend: + volumes: + - $UPLOAD_LOCATION:/data + - /etc/localtime:/etc/localtime:ro + # --- immich devices --- # + labels: + - traefik.enable=true + - traefik.http.services.immich.loadbalancer.server.port=2283 + - traefik.http.services.immich.loadbalancer.server.scheme=http + - traefik.http.routers.immich-https.entrypoints=websecure + - traefik.http.routers.immich-https.rule=Host(`immich.$DOMAIN_NAME`) + - traefik.http.routers.immich-https.tls=true + - traefik.http.routers.immich-https.tls.certresolver=cloudflare + env_file: + - .env + depends_on: + - immich-redis + - immich-database + restart: always + healthcheck: + disable: false + + immich-machine-learning: + container_name: immich-machine-learning + image: ghcr.io/immich-app/immich-machine-learning:$IMMICH_VERSION + networks: + immich_backend: + volumes: + - ${config-dir}/models:/cache + env_file: + - .env + restart: always + healthcheck: + disable: false + + immich-redis: + container_name: immich-redis + image: docker.io/valkey/valkey:8-bookworm@sha256:a137a2b60aca1a75130022d6bb96af423fefae4eb55faf395732db3544803280 + networks: + immich_backend: + healthcheck: + test: redis-cli ping || exit 1 + restart: always + + immich-database: + container_name: immich-database + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:32324a2f41df5de9efe1af166b7008c3f55646f8d0e00d9550c16c9822366b4a + networks: + immich_backend: + shm_size: 128mb + volumes: + # Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file + - $DB_DATA_LOCATION:/var/lib/postgresql/data + environment: + POSTGRES_PASSWORD: $DB_PASSWORD + POSTGRES_USER: $DB_USERNAME + POSTGRES_DB: $DB_DATABASE_NAME + POSTGRES_INITDB_ARGS: '--data-checksums' + restart: always + healthcheck: + disable: false + + networks: + immich_backend: + external: true + immich_frontend: + external: true + ''; + + systemd.services.immich = { + description = "Docker container : ${container_name}"; + after = [ "network.target" "docker.service" "docker.socket" "traefik.service" ]; + requires = [ "docker.service" ]; + wantedBy = ["multi-user.target"]; + path = [ pkgs.docker ]; + + serviceConfig = { + Type = "exec"; + # Pull the latest image before running + ExecStartPre = "${pkgs.docker}/bin/docker compose -f /etc/${compose-dir}/compose.yaml pull"; + # Bring the service up + ExecStart = "${pkgs.docker}/bin/docker compose -f /etc/${compose-dir}/compose.yaml up --remove-orphans"; + # Take it down gracefully + ExecStop = "${pkgs.docker}/bin/docker compose -f /etc/${compose-dir}/compose.yaml down"; + + Restart = "on-failure"; + }; + }; + }; +} \ No newline at end of file diff --git a/docker/passbolt.original b/docker/passbolt.original index c27972a..f492415 100644 --- a/docker/passbolt.original +++ b/docker/passbolt.original @@ -13,20 +13,6 @@ in */ '' services: - passbolt-database: - image: mariadb:11.3 - container_name: passbolt-database - networks: - passbolt_backend: - volumes: - - passbolt-database:/var/lib/mysql - environment: - MYSQL_RANDOM_ROOT_PASSWORD: "true" - MYSQL_DATABASE: $PASSBOLT_MYSQL_DATABASE - MYSQL_USER: $PASSBOLT_MYSQL_USER - MYSQL_PASSWORD: $PASSBOLT_MYSQL_PASSWORD - restart: unless-stopped - passbolt: image: passbolt/passbolt:latest-ce-non-root container_name: passbolt @@ -72,14 +58,23 @@ in - passbolt-database restart: unless-stopped + passbolt-database: + image: mariadb:11.3 + container_name: passbolt-database + networks: + passbolt_backend: + volumes: + - passbolt-database:/var/lib/mysql + environment: + MYSQL_RANDOM_ROOT_PASSWORD: "true" + MYSQL_DATABASE: $PASSBOLT_MYSQL_DATABASE + MYSQL_USER: $PASSBOLT_MYSQL_USER + MYSQL_PASSWORD: $PASSBOLT_MYSQL_PASSWORD + restart: unless-stopped + networks: passbolt_backend: - name: passbolt_backend - driver: bridge - ipam: - config: - - subnet: "172.16.2.0/24" - gateway: "172.16.2.254" + external: true passbolt_frontend: external: true diff --git a/docker/traefik.original b/docker/traefik.original index 0e37626..6f9d66f 100644 --- a/docker/traefik.original +++ b/docker/traefik.original @@ -55,6 +55,13 @@ in config: - subnet: "172.16.1.0/24" gateway: "172.16.1.254" + passbolt_backend: + name: passbolt_backend + driver: bridge + ipam: + config: + - subnet: "172.16.2.0/24" + gateway: "172.16.2.254" passbolt_frontend: name: passbolt_frontend driver: bridge @@ -69,6 +76,13 @@ in config: - subnet: "172.16.3.0/24" gateway: "172.16.3.254" + hass_backend: + name: hass_backend + driver: bridge + ipam: + config: + - subnet: "172.16.4.0/24" + gateway: "172.16.4.254" hass_frontend: name: hass_frontend driver: bridge @@ -76,6 +90,13 @@ in config: - subnet: "172.16.40.0/24" gateway: "172.16.40.254" + immich_backend: + name: immich_backend + driver: bridge + ipam: + config: + - subnet: "172.16.5.0/24" + gateway: "172.16.5.254" immich_frontend: name: immich_frontend driver: bridge diff --git a/ensure-pcr.nix b/ensure-pcr.nix new file mode 100644 index 0000000..d4c889e --- /dev/null +++ b/ensure-pcr.nix @@ -0,0 +1,123 @@ +{ lib, utils, config, ... }: +let + inherit (lib) + head + optional + foldl' + nameValuePair + listToAttrs + optionals + concatStringsSep + sortOn + mkIf + mkEnableOption + mkOption + types + ; +in +{ + options = { + systemIdentity = { + enable = mkEnableOption "hashing of Luks values into PCR 15 and subsequent checks"; + pcr15 = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The expected value of PCR 15 after all luks partitions have been unlocked + Should be a 64 character hex string as ouput by the sha256 field of + 'systemd-analyze pcrs 15 --json=short' + If set to null (the default) it will not check the value. + If the check fails the boot will abort and you will be dropped into an emergency shell, if enabled. + In ermergency shell type: + 'systemctl disable check-pcrs' + 'systemctl default' + to continue booting + ''; + example = "6214de8c3d861c4b451acc8c4e24294c95d55bcec516bbf15c077ca3bffb6547"; + }; + }; + boot.initrd.luks.devices = lib.mkOption { + type = + with lib.types; + attrsOf (submodule { + config.crypttabExtraOpts = optionals config.systemIdentity.enable [ + "tpm2-device=auto" + "tpm2-measure-pcr=yes" + ]; + }); + }; + }; + config = mkIf config.systemIdentity.enable { + boot.kernelParams = [ + "rd.luks=no" + ]; + boot.initrd.systemd.services = + { + check-pcrs = mkIf (config.systemIdentity.pcr15 != null) { + script = '' + echo "Checking PCR 15 value" + if [[ $(systemd-analyze pcrs 15 --json=short | jq -r ".[0].sha256") != "${config.systemIdentity.pcr15}" ]] ; then + echo "PCR 15 check failed" + exit 1 + else + echo "PCR 15 check suceed" + fi + ''; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + unitConfig.DefaultDependencies = "no"; + after = [ "cryptsetup.target" ]; + before = [ "sysroot.mount" ]; + requiredBy = [ "sysroot.mount" ]; + }; + } + // (listToAttrs ( + foldl' ( + acc: attrs: + let + extraOpts = attrs.value.crypttabExtraOpts ++ (optional attrs.value.allowDiscards "discard"); + cfg = config.boot.initrd.systemd; + in + [ + (nameValuePair "cryptsetup-${attrs.name}" { + unitConfig = { + Description = "Cryptography setup for ${attrs.name}"; + DefaultDependencies = "no"; + IgnoreOnIsolate = true; + Conflicts = [ "umount.target" ]; + BindsTo = "${utils.escapeSystemdPath attrs.value.device}.device"; + }; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + TimeoutSec = "infinity"; + KeyringMode = "shared"; + OOMScoreAdjust = 500; + ImportCredential = "cryptsetup.*"; + ExecStart = ''${cfg.package}/bin/systemd-cryptsetup attach '${attrs.name}' '${attrs.value.device}' '-' '${concatStringsSep "," extraOpts}' ''; + ExecStop = ''${cfg.package}/bin/systemd-cryptsetup detach '${attrs.name}' ''; + }; + after = + [ + "cryptsetup-pre.target" + "systemd-udevd-kernel.socket" + "${utils.escapeSystemdPath attrs.value.device}.device" + ] + ++ (optional cfg.tpm2.enable "systemd-tpm2-setup-early.service") + ++ optional (acc != [ ]) "${(head acc).name}.service"; + before = [ + "blockdev@dev-mapper-${attrs.name}.target" + "cryptsetup.target" + "umount.target" + ]; + wants = [ "blockdev@dev-mapper-${attrs.name}.target" ]; + requiredBy = [ "sysroot.mount" ]; + }) + ] + ++ acc + ) [ ] (sortOn (x: x.name) (lib.attrsets.attrsToList config.boot.initrd.luks.devices)) + )); + }; +}