configs: permissions_actions_data: content: >- [{"read_only": false, "mount_path": "/mnt/permission/redis_redis_data", "is_temporary": true, "identifier": "redis_redis_data", "recursive": true, "mode": "check", "uid": 568, "gid": 568, "chmod": null}, {"read_only": false, "mount_path": "/mnt/permission/ml-cache", "is_temporary": true, "identifier": "ml-cache", "recursive": true, "mode": "check", "uid": 0, "gid": 0, "chmod": null}] permissions_run_script: content: | #!/usr/bin/env python3 import os import json import time import shutil with open("/script/actions.json", "r") as f: actions_data = json.load(f) if not actions_data: # If this script is called, there should be actions data raise ValueError("No actions data found") def fix_perms(path, chmod, recursive=False): print(f"Changing permissions{' recursively ' if recursive else ' '}to {chmod} on: [{path}]") os.chmod(path, int(chmod, 8)) if recursive: for root, dirs, files in os.walk(path): for f in files: os.chmod(os.path.join(root, f), int(chmod, 8)) print("Permissions after changes:") print_chmod_stat() def fix_owner(path, uid, gid, recursive=False): print(f"Changing ownership{' recursively ' if recursive else ' '}to {uid}:{gid} on: [{path}]") os.chown(path, uid, gid) if recursive: for root, dirs, files in os.walk(path): for f in files: os.chown(os.path.join(root, f), uid, gid) print("Ownership after changes:") print_chown_stat() def print_chown_stat(): curr_stat = os.stat(action["mount_path"]) print(f"Ownership: [{curr_stat.st_uid}:{curr_stat.st_gid}]") def print_chmod_stat(): curr_stat = os.stat(action["mount_path"]) print(f"Permissions: [{oct(curr_stat.st_mode)[3:]}]") def print_chown_diff(curr_stat, uid, gid): print(f"Ownership: wanted [{uid}:{gid}], got [{curr_stat.st_uid}:{curr_stat.st_gid}].") def print_chmod_diff(curr_stat, mode): print(f"Permissions: wanted [{mode}], got [{oct(curr_stat.st_mode)[3:]}].") def perform_action(action): if action["read_only"]: print(f"Path for action [{action['identifier']}] is read-only, skipping...") return start_time = time.time() print(f"=== Applying configuration on volume with identifier [{action['identifier']}] ===") if not os.path.isdir(action["mount_path"]): print(f"Path [{action['mount_path']}] is not a directory, skipping...") return if action["is_temporary"]: print(f"Path [{action['mount_path']}] is a temporary directory, ensuring it is empty...") for item in os.listdir(action["mount_path"]): item_path = os.path.join(action["mount_path"], item) # Exclude the safe directory, where we can use to mount files temporarily if os.path.basename(item_path) == "ix-safe": continue if os.path.isdir(item_path): shutil.rmtree(item_path) else: os.remove(item_path) if not action["is_temporary"] and os.listdir(action["mount_path"]): print(f"Path [{action['mount_path']}] is not empty, skipping...") return print(f"Current Ownership and Permissions on [{action['mount_path']}]:") curr_stat = os.stat(action["mount_path"]) print_chown_diff(curr_stat, action["uid"], action["gid"]) print_chmod_diff(curr_stat, action["chmod"]) print("---") if action["mode"] == "always": fix_owner(action["mount_path"], action["uid"], action["gid"], action["recursive"]) if not action["chmod"]: print("Skipping permissions check, chmod is falsy") else: fix_perms(action["mount_path"], action["chmod"], action["recursive"]) return elif action["mode"] == "check": if curr_stat.st_uid != action["uid"] or curr_stat.st_gid != action["gid"]: print("Ownership is incorrect. Fixing...") fix_owner(action["mount_path"], action["uid"], action["gid"], action["recursive"]) else: print("Ownership is correct. Skipping...") if not action["chmod"]: print("Skipping permissions check, chmod is falsy") else: if oct(curr_stat.st_mode)[3:] != action["chmod"]: print("Permissions are incorrect. Fixing...") fix_perms(action["mount_path"], action["chmod"], action["recursive"]) else: print("Permissions are correct. Skipping...") print(f"Time taken: {(time.time() - start_time) * 1000:.2f}ms") print(f"=== Finished applying configuration on volume with identifier [{action['identifier']}] ==") print() if __name__ == "__main__": start_time = time.time() for action in actions_data: perform_action(action) print(f"Total time taken: {(time.time() - start_time) * 1000:.2f}ms") services: machine-learning: cap_drop: - ALL depends_on: permissions: condition: service_completed_successfully deploy: resources: limits: cpus: '2' memory: 4096M devices: - /dev/dri:/dev/dri environment: HF_HOME: /mlcache IMMICH_LOG_LEVEL: log IMMICH_PORT: '32002' MACHINE_LEARNING_CACHE_FOLDER: /mlcache MPLCONFIGDIR: /mlcache/matplotlib NODE_ENV: production NVIDIA_VISIBLE_DEVICES: void TRANSFORMERS_CACHE: /mlcache TZ: America/New_York UMASK: '002' UMASK_SET: '002' group_add: - 44 - 107 - 568 healthcheck: interval: 30s retries: 5 start_interval: 2s start_period: 15s test: python3 healthcheck.py timeout: 5s image: ghcr.io/immich-app/immich-machine-learning:v1.141.1 platform: linux/amd64 privileged: False restart: unless-stopped security_opt: - no-new-privileges=true stdin_open: False tty: False user: '0:0' volumes: - read_only: False source: ml-cache target: /mlcache type: volume volume: nocopy: False permissions: cap_add: - CHOWN - DAC_OVERRIDE - FOWNER cap_drop: - ALL configs: - mode: 320 source: permissions_actions_data target: /script/actions.json - mode: 448 source: permissions_run_script target: /script/run.py deploy: resources: limits: cpus: '2' memory: 1024M entrypoint: - python3 - /script/run.py environment: NVIDIA_VISIBLE_DEVICES: void TZ: America/New_York UMASK: '002' UMASK_SET: '002' group_add: - 568 healthcheck: disable: True image: python:3.13.0-slim-bookworm network_mode: none platform: linux/amd64 privileged: False restart: on-failure:1 security_opt: - no-new-privileges=true stdin_open: False tty: False user: '0:0' volumes: - read_only: False source: ml-cache target: /mnt/permission/ml-cache type: volume volume: nocopy: False - read_only: False source: redis-data target: /mnt/permission/redis_redis_data type: volume volume: nocopy: False pgvecto: cap_drop: - ALL depends_on: permissions: condition: service_completed_successfully deploy: resources: limits: cpus: '2' memory: 4096M environment: DB_STORAGE_TYPE: SSD NVIDIA_VISIBLE_DEVICES: void PGPORT: '5432' POSTGRES_DB: immich POSTGRES_PASSWORD: REDACTED POSTGRES_USER: immich TZ: America/New_York UMASK: '002' UMASK_SET: '002' group_add: - 568 healthcheck: interval: 30s retries: 5 start_interval: 2s start_period: 15s test: - CMD - pg_isready - '-h' - 127.0.0.1 - '-p' - '5432' - '-U' - immich - '-d' - immich timeout: 5s image: ghcr.io/immich-app/postgres:15-vectorchord0.4.3-pgvectors0.2.0 platform: linux/amd64 privileged: False restart: unless-stopped security_opt: - no-new-privileges=true shm_size: 128M stdin_open: False tty: False user: '999:999' volumes: - bind: create_host_path: False propagation: rprivate read_only: False source: /mnt/primary/apps/immich/postgres_data target: /var/lib/postgresql/data type: bind redis: cap_drop: - ALL command: - '--port' - '6379' - '--requirepass' - REDACTED depends_on: permissions: condition: service_completed_successfully deploy: resources: limits: cpus: '2' memory: 4096M environment: NVIDIA_VISIBLE_DEVICES: void REDIS_PASSWORD: REDACTED TZ: America/New_York UMASK: '002' UMASK_SET: '002' group_add: - 568 healthcheck: interval: 30s retries: 5 start_interval: 2s start_period: 15s test: - CMD - redis-cli - '-h' - 127.0.0.1 - '-p' - '6379' - '-a' - REDACTED - ping timeout: 5s image: valkey/valkey:8.1.3 platform: linux/amd64 privileged: False restart: unless-stopped security_opt: - no-new-privileges=true stdin_open: False tty: False user: '568:568' volumes: - read_only: False source: redis-data target: /data type: volume volume: nocopy: False server: cap_drop: - ALL depends_on: permissions: condition: service_completed_successfully pgvecto: condition: service_healthy redis: condition: service_healthy deploy: resources: limits: cpus: '2' memory: 4096M devices: - /dev/dri:/dev/dri environment: DB_DATABASE_NAME: immich DB_HOSTNAME: pgvecto DB_PASSWORD: REDACTED DB_PORT: '5432' DB_USERNAME: immich IMMICH_LOG_LEVEL: log IMMICH_MACHINE_LEARNING_ENABLED: 'true' IMMICH_MACHINE_LEARNING_URL: http://machine-learning:32002 IMMICH_PORT: '30041' NODE_ENV: production NVIDIA_VISIBLE_DEVICES: void REDIS_DBINDEX: '0' REDIS_HOSTNAME: redis REDIS_PASSWORD: REDACTED REDIS_PORT: '6379' TZ: America/New_York UMASK: '002' UMASK_SET: '002' group_add: - 44 - 107 - 568 healthcheck: interval: 30s retries: 5 start_interval: 2s start_period: 15s test: immich-healthcheck timeout: 5s image: ghcr.io/immich-app/immich-server:v1.141.1 platform: linux/amd64 ports: - mode: ingress protocol: tcp published: 30041 target: 30041 privileged: False restart: unless-stopped security_opt: - no-new-privileges=true stdin_open: False tty: False user: '0:0' volumes: - bind: create_host_path: False propagation: rprivate read_only: False source: /mnt/primary/apps/immich_data target: /data type: bind volumes: ml-cache: {} redis-data: {} x-notes: >+ # Immich ## Security ### Container: [server] - Is running as root user - Is running as root group ### Container: [machine-learning] - Is running as root user - Is running as root group ### Container: [permissions] **This container is short-lived.** - Is running as root user - Is running as root group ## Bug Reports and Feature Requests If you find a bug in this app or have an idea for a new feature, please file an issue at https://github.com/truenas/apps x-portals: - host: 0.0.0.0 name: Web UI path: / port: 30041 scheme: http