Documents the FolderSync/PhotoSync → UNAS Pro → Immich external library pattern as the workaround for immich-app/immich#4282 (deleted photos re-uploading on next mobile sync). Includes all steps: UNAS share creation, Proxmox NFS mount, LXC bind mount, docker-compose volume, Immich library import path update, and per-app config for Android (FolderSync) and iOS (PhotoSync). - Adds docs/immich/mobile-sync.md with full step-by-step guide - Adds mobile_photos to share map in docs/unas/nfs-mounts.md - Adds IMMICH_MOBILE_SYNC_DIR volume (/mnt/mobile:ro) to template.yaml - Documents IMMICH_MOBILE_SYNC_DIR in README.md and .env.sample
5.7 KiB
Immich
Self-hosted photo and video management solution with high performance and visual aesthetics.
Prerequisites
- NVIDIA GPU: This template is configured for NVIDIA GPU acceleration (RTX A400).
- Proxmox LXC: Must be a Privileged LXC with NVIDIA drivers and Container Toolkit installed.
- Storage: NFS-backed storage for photo uploads is recommended.
Deployment
-
Update
.env: Ensure the Immich variables are set in your root.envfile.IMMICH_UPLOAD_LOCATIONshould point to your dedicated Immich share on the UNAS:IMMICH_UPLOAD_LOCATION=/mnt/unas/immich_data IMMICH_DATA_DIR=/opt/immich/appdata IMMICH_EXTERNAL_LIBRARY=/mnt/unas/immich_data/upload/upload IMMICH_MOBILE_SYNC_DIR=/mnt/unas/mobile_photos IMMICH_VERSION=release IMMICH_DB_PASSWORD=<your-secure-password> IMMICH_DB_USERNAME=immich IMMICH_DB_DATABASE_NAME=immich IMMICH_DB_HOSTNAME=database IMMICH_REDIS_HOSTNAME=redisOptional OIDC settings can also be supplied. Set
OIDC_ENABLED=trueexplicitly when enabling OAuth; leaving it unset keeps Immich local-login only. Zitadel with PKCE does not require a client secret — leaveOIDC_CLIENT_SECRETempty.OIDC_ENABLED=true OIDC_ISSUER=https://zitadel.example.com/ OIDC_CLIENT_ID=<immich-client-id> OIDC_CLIENT_SECRET= OIDC_NAME=<button-label> OIDC_AUTO_REDIRECT=false OIDC_MOBILE_OVERRIDE_ENABLED=false OIDC_DISABLE_LOCAL_AUTH=falseOnce OIDC is confirmed working, set
OIDC_DISABLE_LOCAL_AUTH=trueand restart the server to enforce SSO-only login:sed -i 's/DISABLE_PASSWORD_LOGIN=false/DISABLE_PASSWORD_LOGIN=true/' /opt/immich/docker-compose.yml docker compose -f /opt/immich/docker-compose.yml up -d --no-deps --force-recreate immich-server -
Create Directories: Create the local appdata directories and the shared media directory:
# Local config/database storage (matches IMMICH_DATA_DIR) sudo mkdir -p /opt/immich/appdata/{postgres,model-cache} sudo chown -R 1000:1000 /opt/immich/appdata # Separate UNAS share for photos sudo mkdir -p /mnt/unas/immich_data sudo chown -R 1000:1000 /mnt/unas/immich_data -
Mounting the Share: Ensure you have created a separate NFS share on your UNAS (e.g.,
immich_data) and mounted it on your Proxmox host at/mnt/unas/immich_databefore starting the containers. -
Render Compose File:
./substitute_env.sh docker-compose-files/immich/template.yaml docker-compose.immich.yml .env -
Start Stack:
docker compose -f docker-compose.immich.yml up -d
External Library Setup
After the stack is running, create an external library via the API to import photos from IMMICH_EXTERNAL_LIBRARY (mounted read-only at /mnt/external inside the container):
TOKEN=$(curl -s -X POST http://<immich-ip>:2283/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"<email>","password":"<password>"}' \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["accessToken"])')
USER_ID=$(curl -s http://<immich-ip>:2283/api/users/me \
-H "Authorization: Bearer $TOKEN" \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
LIB_ID=$(curl -s -X POST http://<immich-ip>:2283/api/libraries \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d "{\"name\":\"NAS Photos\",\"ownerId\":\"$USER_ID\",\"importPaths\":[\"/mnt/external/<user-uuid>\"]}" \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
curl -s -X POST http://<immich-ip>:2283/api/libraries/$LIB_ID/scan \
-H "Authorization: Bearer $TOKEN"
The <user-uuid> is the Immich user's UUID — find it at Settings → Account in the Immich UI, or via:
curl -s http://<immich-ip>:2283/api/users/me -H "Authorization: Bearer $TOKEN" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])'
Important
Immich rejects any external library import path that falls under
/data/upload/(the upload volume). The separate/mnt/externalmount exists specifically to work around this restriction — the same photos are accessible at both paths inside the container, but only/mnt/externalis accepted for external libraries.
Privileged Mode Rationale
Immich is deployed in a Privileged LXC to ensure:
- GPU Access: The
immich-machine-learningcontainer requires direct access to NVIDIA device nodes for CUDA compute. - VRAM Management: CUDA memory allocation and GSP firmware communication are more stable in privileged mode.
- Storage Mapping: Simplified 1:1 UID/GID mapping for photo uploads on the UNAS NFS share.
GPU Verification
Both immich-server and immich-machine-learning run with runtime: nvidia:
- immich-server — uses NVENC for hardware-accelerated H.264 video transcoding (
NVIDIA_DRIVER_CAPABILITIES=video,compute,utility) - immich-machine-learning — uses the
:release-cudaimage withCUDAExecutionProviderfor CLIP embeddings, face detection, and OCR
Verify GPU access after startup:
# Confirm NVIDIA device visible in server (needed for NVENC)
docker exec immich_server ls /dev/nvidia0
# Confirm CUDA ONNX provider active in ML container
docker exec immich_machine_learning python3 -c 'import onnxruntime; print(onnxruntime.get_available_providers())'
# Expected: ['TensorrtExecutionProvider', 'CUDAExecutionProvider', 'CPUExecutionProvider']
After verifying, go to Administration → Video Transcoding in the Immich UI and confirm Hardware Acceleration is set to NVENC. If it shows QSV (from a prior config), change it to NVENC and re-run the Video Conversion job.