diff --git a/kubernetes/apps/database/cloudnative-pg/templates/cluster.yaml b/kubernetes/apps/database/cloudnative-pg/templates/cluster.yaml index dc6f7cb3a..9cdaecefb 100644 --- a/kubernetes/apps/database/cloudnative-pg/templates/cluster.yaml +++ b/kubernetes/apps/database/cloudnative-pg/templates/cluster.yaml @@ -11,7 +11,7 @@ spec: instances: 2 # yamllint disable rule:line-length # renovate: datasource=docker depName=ghcr.io/cloudnative-pg/postgresql versioning=redhat - imageName: "ghcr.io/cloudnative-pg/postgresql:16.4-5" + imageName: "ghcr.io/tensorchord/cloudnative-pgvecto.rs:16.4-v0.3.0" # yamllint enable rule:line-length enableSuperuserAccess: true inheritedMetadata: @@ -27,6 +27,8 @@ spec: recovery: source: &previousCluster postgres-v4 postgresql: + shared_preload_libraries: + - "vectors.so" parameters: max_connections: "600" shared_buffers: 512MB diff --git a/kubernetes/apps/default/immich/Chart.yaml b/kubernetes/apps/default/immich/Chart.yaml new file mode 100644 index 000000000..cd16df2fe --- /dev/null +++ b/kubernetes/apps/default/immich/Chart.yaml @@ -0,0 +1,10 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/chart.json +apiVersion: v2 +name: immich +version: 1.0.0 +type: application +dependencies: + - name: app-template + version: 3.4.0 + repository: https://bjw-s.github.io/helm-charts diff --git a/kubernetes/apps/default/immich/application.yaml b/kubernetes/apps/default/immich/application.yaml new file mode 100644 index 000000000..99be5e4ae --- /dev/null +++ b/kubernetes/apps/default/immich/application.yaml @@ -0,0 +1,28 @@ +--- +# yaml-language-server: $schema=https://deedee-ops.github.io/schemas/argoproj.io/application_v1alpha1.json +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: immich + namespace: argocd +spec: + project: default + sources: + - repoURL: https://github.com/deedee-ops/home-ops.git + targetRevision: master + path: kubernetes/apps/default/immich + plugin: + name: argocd-vault-plugin-helm + destination: + namespace: default + server: https://kubernetes.default.svc + syncPolicy: + automated: + prune: true + syncOptions: + - CreateNamespace=true + managedNamespaceMetadata: + labels: + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/warn: restricted diff --git a/kubernetes/apps/default/immich/files/authelia.yaml b/kubernetes/apps/default/immich/files/authelia.yaml new file mode 100644 index 000000000..5c7d611a4 --- /dev/null +++ b/kubernetes/apps/default/immich/files/authelia.yaml @@ -0,0 +1,24 @@ +--- +# yaml-language-server: disabled +identity_providers: + oidc: + clients: + - client_id: immich + client_name: Immich + # docker run authelia/authelia:latest authelia crypto hash generate pbkdf2 --variant sha512 + # --random --random.length 72 --random.charset rfc3986 + client_secret: '' + consent_mode: 'implicit' + public: false + authorization_policy: 'two_factor' + require_pkce: false + redirect_uris: + - 'app.immich:///oauth-callback' + - 'https://immich./auth/login' + - 'https://immich./user-settings' + scopes: + - 'email' + - 'openid' + - 'profile' + userinfo_signed_response_alg: 'none' + token_endpoint_auth_method: 'client_secret_basic' diff --git a/kubernetes/apps/default/immich/files/config.json b/kubernetes/apps/default/immich/files/config.json new file mode 100644 index 000000000..c0d2e59ed --- /dev/null +++ b/kubernetes/apps/default/immich/files/config.json @@ -0,0 +1,180 @@ +{ + "ffmpeg": { + "crf": 23, + "threads": 0, + "preset": "medium", + "targetVideoCodec": "h264", + "acceptedVideoCodecs": [ + "h264" + ], + "targetAudioCodec": "aac", + "acceptedAudioCodecs": [ + "aac", + "mp3" + ], + "acceptedContainers": [ + "mov", + "ogg", + "webm" + ], + "targetResolution": "1080", + "maxBitrate": "0", + "bframes": -1, + "refs": 0, + "gopSize": 0, + "npl": 0, + "temporalAQ": false, + "cqMode": "auto", + "twoPass": false, + "preferredHwDevice": "auto", + "transcode": "required", + "tonemap": "hable", + "accel": "qsv", + "accelDecode": true + }, + "job": { + "backgroundTask": { + "concurrency": 5 + }, + "smartSearch": { + "concurrency": 2 + }, + "metadataExtraction": { + "concurrency": 5 + }, + "faceDetection": { + "concurrency": 2 + }, + "search": { + "concurrency": 5 + }, + "sidecar": { + "concurrency": 5 + }, + "library": { + "concurrency": 5 + }, + "migration": { + "concurrency": 5 + }, + "thumbnailGeneration": { + "concurrency": 3 + }, + "videoConversion": { + "concurrency": 1 + }, + "notifications": { + "concurrency": 5 + } + }, + "logging": { + "enabled": true, + "level": "log" + }, + "machineLearning": { + "enabled": true, + "url": "http://immich-machine-learning.default.svc.cluster.local:3003", + "clip": { + "enabled": true, + "modelName": "ViT-B-32__openai" + }, + "duplicateDetection": { + "enabled": true, + "maxDistance": 0.01 + }, + "facialRecognition": { + "enabled": false, + "modelName": "buffalo_l", + "minScore": 0.7, + "maxDistance": 0.5, + "minFaces": 3 + } + }, + "map": { + "enabled": true, + "lightStyle": "", + "darkStyle": "" + }, + "reverseGeocoding": { + "enabled": true + }, + "metadata": { + "faces": { + "import": true + } + }, + "oauth": { + "autoLaunch": true, + "autoRegister": false, + "buttonText": "Login with Authelia", + "clientId": "immich", + "clientSecret": "", + "defaultStorageQuota": 0, + "enabled": true, + "issuerUrl": "https://authelia.", + "mobileOverrideEnabled": false, + "mobileRedirectUri": "", + "scope": "openid email profile", + "signingAlgorithm": "RS256", + "profileSigningAlgorithm": "none", + "storageLabelClaim": "preferred_username", + "storageQuotaClaim": "immich_quota" + }, + "passwordLogin": { + "enabled": true + }, + "storageTemplate": { + "enabled": false, + "hashVerificationEnabled": true, + "template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}" + }, + "image": { + "thumbnailFormat": "webp", + "thumbnailSize": 250, + "previewFormat": "jpeg", + "previewSize": 1440, + "quality": 80, + "colorspace": "p3", + "extractEmbedded": false + }, + "newVersionCheck": { + "enabled": false + }, + "trash": { + "enabled": true, + "days": 30 + }, + "theme": { + "customCss": "" + }, + "library": { + "scan": { + "enabled": true, + "cronExpression": "0 0 * * *" + }, + "watch": { + "enabled": false + } + }, + "server": { + "externalDomain": "https://immich.", + "loginPageMessage": "" + }, + "notifications": { + "smtp": { + "enabled": true, + "from": "", + "replyTo": "", + "transport": { + "ignoreCert": false, + "host": "smtp-relay.networking.svc.cluster.local", + "port": 25, + "username": "", + "password": "" + } + } + }, + "user": { + "deleteDelay": 7 + } +} diff --git a/kubernetes/apps/default/immich/templates/authelia.tmpl.yaml b/kubernetes/apps/default/immich/templates/authelia.tmpl.yaml new file mode 100644 index 000000000..b94e7849c --- /dev/null +++ b/kubernetes/apps/default/immich/templates/authelia.tmpl.yaml @@ -0,0 +1,14 @@ +--- +# yamllint disable rule:line-length +# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master-standalone/configmap-v1.json +# yamllint enable rule:line-length +apiVersion: v1 +kind: ConfigMap +metadata: + name: immich-authelia + labels: + authelia.com/enabled: "true" +data: + immich.yaml: | +{{ .Files.Get "files/authelia.yaml" | indent 4 }} + diff --git a/kubernetes/apps/default/immich/templates/config.tmpl.yaml b/kubernetes/apps/default/immich/templates/config.tmpl.yaml new file mode 100644 index 000000000..86c84089a --- /dev/null +++ b/kubernetes/apps/default/immich/templates/config.tmpl.yaml @@ -0,0 +1,13 @@ +--- +# yamllint disable rule:line-length +# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master-standalone/configmap-v1.json +# yamllint enable rule:line-length +apiVersion: v1 +kind: ConfigMap +metadata: + name: immich-configmap +data: + config.json: | +{{ .Files.Get "files/config.json" | indent 4 }} + + diff --git a/kubernetes/apps/default/immich/templates/initdb.yaml b/kubernetes/apps/default/immich/templates/initdb.yaml new file mode 100644 index 000000000..d6c61b636 --- /dev/null +++ b/kubernetes/apps/default/immich/templates/initdb.yaml @@ -0,0 +1,46 @@ +--- +# yamllint disable rule:line-length +# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master-standalone/job.json +# yamllint enable +apiVersion: batch/v1 +kind: Job +metadata: + generateName: immich-init-db- + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: HookSucceeded + argocd.argoproj.io/sync-wave: "-1" +spec: + template: + spec: + restartPolicy: Never + containers: + - name: init-db + image: ghcr.io/deedee-ops/postgres-init:16.4 + volumeMounts: + - mountPath: /secrets + name: secrets + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - ALL + volumes: + - csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: immich + name: secrets + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + +# @todo, automate: +# ALTER DATABASE immich SET search_path TO "$user", public, vectors; +# CREATE EXTENSION IF NOT EXISTS vectors; +# CREATE EXTENSION IF NOT EXISTS earthdistance CASCADE; +# ALTER SCHEMA vectors OWNER TO pg_database_owner; diff --git a/kubernetes/apps/default/immich/templates/network_policy.yaml b/kubernetes/apps/default/immich/templates/network_policy.yaml new file mode 100644 index 000000000..f1d818b7b --- /dev/null +++ b/kubernetes/apps/default/immich/templates/network_policy.yaml @@ -0,0 +1,22 @@ +--- +# yaml-language-server: $schema=https://deedee-ops.github.io/schemas/cilium.io/ciliumnetworkpolicy_v2.json +apiVersion: "cilium.io/v2" +kind: CiliumNetworkPolicy +metadata: + name: immich +specs: + - endpointSelector: + matchLabels: + app.kubernetes.io/name: immich + app.kubernetes.io/component: machine-learning + egress: + - toPorts: + - ports: + - port: "53" + protocol: ANY + rules: + dns: + - matchPattern: "*" + - toFQDNs: + - matchName: huggingface.co + - matchPattern: "*.huggingface.co" diff --git a/kubernetes/apps/default/immich/templates/pvc.yaml b/kubernetes/apps/default/immich/templates/pvc.yaml new file mode 100644 index 000000000..a57da6450 --- /dev/null +++ b/kubernetes/apps/default/immich/templates/pvc.yaml @@ -0,0 +1,32 @@ +# yamllint disable rule:line-length +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master-standalone/persistentvolumeclaim.json +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: immich-external + annotations: + nfs.io/storage-path: photos +spec: + storageClassName: nfs-client-media + accessModes: + - ReadWriteMany + resources: + requests: + storage: 7Ti # use rough size of NAS NFS volume to silence "volume filling up" alerts +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/master-standalone/persistentvolumeclaim.json +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: immich-data + annotations: + nfs.io/storage-path: immich +spec: + storageClassName: nfs-client-kubernetes + accessModes: + - ReadWriteMany + resources: + requests: + storage: 7Ti # use rough size of NAS NFS volume to silence "volume filling up" alerts +# yamllint enable rule:line-length diff --git a/kubernetes/apps/default/immich/templates/secret_class.yaml b/kubernetes/apps/default/immich/templates/secret_class.yaml new file mode 100644 index 000000000..3e75227f8 --- /dev/null +++ b/kubernetes/apps/default/immich/templates/secret_class.yaml @@ -0,0 +1,38 @@ +--- +# yamllint disable rule:line-length +# yaml-language-server: $schema=https://deedee-ops.github.io/schemas/secrets-store.csi.x-k8s.io/secretproviderclass_v1.json +# yamllint enable rule:line-length +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: immich + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + argocd.argoproj.io/sync-wave: "-2" +spec: + provider: vault + parameters: + objects: | + # initdb + - objectName: "INIT_POSTGRES_USER" + secretPath: "kubernetes/data/internal/immich" + secretKey: "DB_USERNAME" + - objectName: "INIT_POSTGRES_PASS" + secretPath: "kubernetes/data/internal/immich" + secretKey: "DB_PASSWORD" + - objectName: "INIT_POSTGRES_HOST" + secretPath: "kubernetes/data/internal/cloudnative-pg" + secretKey: "HOST" + - objectName: "INIT_POSTGRES_DBNAME" + secretPath: "kubernetes/data/internal/immich" + secretKey: "DB_DATABASE" + - objectName: "INIT_POSTGRES_SUPER_USER" + secretPath: "kubernetes/data/internal/cloudnative-pg" + secretKey: "SUPERUSER_USERNAME" + - objectName: "INIT_POSTGRES_SUPER_PASS" + secretPath: "kubernetes/data/internal/cloudnative-pg" + secretKey: "SUPERUSER_PASSWORD" + roleName: default + vaultAddress: https://vault.tools:8200 + vaultCACertPath: /vault/tls/tls.ca diff --git a/kubernetes/apps/default/immich/values.yaml b/kubernetes/apps/default/immich/values.yaml new file mode 100644 index 000000000..5d3f833a4 --- /dev/null +++ b/kubernetes/apps/default/immich/values.yaml @@ -0,0 +1,355 @@ +--- +# yaml-language-server: $schema=https://deedee-ops.github.io/schemas/custom/bernd-schorgers-apptemplate.json +app-template: + defaultPodOptions: + securityContext: + fsGroup: 65000 + runAsUser: 65000 + runAsGroup: 65000 + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + + controllers: + machine-learning: + annotations: + reloader.stakater.com/auto: "true" + + replicas: 3 + strategy: RollingUpdate + pod: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: immich + app.kubernetes.io/instance: immich + app.kubernetes.io/component: machine-learning + + containers: + app: + image: + repository: ghcr.io/immich-app/immich-machine-learning + tag: v1.114.0 + pullPolicy: IfNotPresent + + env: &env + DB_DATABASE_NAME: "" + DB_HOSTNAME: "" + DB_PASSWORD: "" + DB_USERNAME: "" + IMMICH_CONFIG_FILE: /config/config.json + IMMICH_ENV: production + IMMICH_MACHINE_LEARNING_URL: http://immich-machine-learning.default.svc.cluster.local:3003 + IMMICH_MEDIA_LOCATION: /data + IMMICH_METRICS: true + IMMICH_SERVER_URL: http://immich-server.default.svc.cluster.local:3001 + MPLCONFIGDIR: /cache/matplotlib + NODE_ENV: production + REDIS_HOSTNAME: redis-ha-haproxy.database.svc.cluster.local + REDIS_PASSWORD: "" + REDIS_PORT: 6379 + TRANSFORMERS_CACHE: /cache + TZ: Europe/Warsaw + + securityContext: &securityContext + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + + probes: + liveness: &mlprobes + enabled: true + custom: true + spec: + httpGet: + path: /ping + port: 3003 + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + readiness: *mlprobes + startup: + enabled: false + + resources: + requests: + cpu: 10m + memory: 1Gi + limits: + memory: 6Gi + + server: + annotations: + reloader.stakater.com/auto: "true" + + containers: + app: + image: + repository: ghcr.io/immich-app/immich-server + tag: v1.114.0 + pullPolicy: IfNotPresent + + command: &cmd + - tini + - "--" + - node + - /usr/src/app/dist/main + args: immich + + env: *env + securityContext: *securityContext + + probes: + liveness: &srvprobes + enabled: true + custom: true + spec: + httpGet: + path: /api/server-info/ping + port: 3001 + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + readiness: *srvprobes + startup: + enabled: true + custom: true + spec: + httpGet: + path: /api/server-info/ping + port: 3001 + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 30 + + resources: + requests: + cpu: 10m + memory: 128Mi + limits: + memory: 2Gi + + microservices: + annotations: + reloader.stakater.com/auto: "true" + + replicas: 3 + strategy: RollingUpdate + pod: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: immich + app.kubernetes.io/instance: immich + app.kubernetes.io/component: microservices + + containers: + app: + image: + repository: ghcr.io/immich-app/immich-server + tag: v1.114.0 + pullPolicy: IfNotPresent + + command: *cmd + args: microservices + + env: *env + securityContext: *securityContext + + resources: + requests: + cpu: 10m + memory: 128Mi + gpu.intel.com/i915: 1 + limits: + memory: 2Gi + gpu.intel.com/i915: 1 + + album-creator: + type: cronjob + cronjob: + schedule: "0 0 * * *" # once a day + successfulJobsHistory: 0 + failedJobsHistory: 5 + pod: + securityContext: + runAsUser: 65000 + runAsGroup: 65000 + fsGroup: 65000 + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + app: + image: + repository: ghcr.io/salvoxia/immich-folder-album-creator + tag: 0.9.0 + pullPolicy: IfNotPresent + command: + - /script/immich_auto_album.sh + env: + ALBUM_LEVELS: 1 + API_KEY: "" + API_URL: http://immich-server.default.svc.cluster.local:3001/api/ + MODE: CREATE + ROOT_PATH: /external + SYNC_MODE: 1 + UNATTENDED: 1 + + securityContext: *securityContext + + probes: + readiness: &probes + enabled: true + custom: true + spec: + exec: + command: + - pgrep + - -f + - immich_auto_album + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + liveness: *probes + + resources: + requests: + cpu: 10m + memory: 20Mi + limits: + memory: 128Mi + + service: + server: + controller: server + ports: + http: + port: 3001 + protocol: HTTP + metrics: + port: 8081 + protocol: HTTP + microservices: + controller: microservices + ports: + metrics: + port: 8082 + protocol: HTTP + machine-learning: + controller: machine-learning + ports: + http: + port: 3003 + protocol: HTTP + + serviceMonitor: + server: + serviceName: immich-server + endpoints: + - port: metrics + scheme: http + microservices: + serviceName: immich-microservices + endpoints: + - port: metrics + scheme: http + + ingress: + server: + className: internal + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/group: Media + gethomepage.dev/name: Immich + gethomepage.dev/icon: immich.png + gethomepage.dev/description: Photo Library + nginx.ingress.kubernetes.io/proxy-body-size: "0" + nginx.ingress.kubernetes.io/configuration-snippet: >- + more_set_headers "Content-Security-Policy: + default-src 'self' 'unsafe-eval' 'wasm-unsafe-eval' 'unsafe-inline' data: mediastream: blob: wss: + https://static.immich.cloud https://tiles.immich.cloud + https://*.; object-src 'none'; + report-uri https://csp./report/csp/audiobookshelf;"; + hosts: + - host: "immich." + paths: + - path: / + pathType: Prefix + service: + identifier: server + port: 3001 + tls: + - hosts: + - "immich." + api: + className: internal + annotations: + nginx.ingress.kubernetes.io/enable-global-auth: "false" + hosts: + - host: "immich." + paths: + - path: /api + pathType: Prefix + service: + identifier: server + port: 3001 + tls: + - hosts: + - "immich." + + persistence: + cache: + type: persistentVolumeClaim + storageClass: ceph-filesystem + size: 10Gi + accessMode: ReadWriteMany + advancedMounts: + machine-learning: + app: + - path: "/cache" + config-file: + type: configMap + name: immich-configmap + advancedMounts: + server: &vol-config + app: + - path: /config/config.json + subPath: config.json + readOnly: true + microservices: *vol-config + data: + type: persistentVolumeClaim + existingClaim: immich-data + advancedMounts: + server: &vol-data + app: + - path: /data + microservices: *vol-data + external: + type: persistentVolumeClaim + existingClaim: immich-external + advancedMounts: + album-creator: &vol-external + app: + - path: "/external" + readOnly: true + server: *vol-external + microservices: *vol-external + tmpdir: + type: emptyDir + medium: Memory + globalMounts: + - path: /tmp