Here is the complete, professional deployment guide for the split-storage Immich architecture. This adheres strictly to the security principle of keeping the Proxmox host clean while isolating the database/thumbnails on the SSD and heavy media on the ZFS HDD array.
Run these commands directly on the Proxmox host shell.
1. Create a Dedicated Unprivileged User To prevent mapping the container to a privileged host user, create a "dead" user with no login access.
groupadd -g 2500 immich_storage
useradd -u 2500 -g 2500 -M -s /usr/sbin/nologin immich_storage2. Configure ZFS Dataset Properties (HDD)
Optimize the existing ZFS dataset for large media files and strict security. (Replace tank/data/photos with your actual dataset).
# Security: Neuter the dataset to prevent execution or privilege escalation
zfs set setuid=off tank/data/photos
zfs set devices=off tank/data/photos
zfs set exec=off tank/data/photos
# Performance: Optimize for large binary files (RAID 10)
zfs set recordsize=1M tank/data/photos
zfs set atime=off tank/data/photos
zfs set xattr=sa tank/data/photos3. Apply Permissions
chown 2500:2500 /tank/data/photos
chmod 750 /tank/data/photos1. Create the LXC
- Create an Unprivileged LXC container via the Proxmox UI.
- Place the container's root disk on your SSD storage (e.g., LVM-Thin). Set the disk size large enough to hold your database, machine learning models, and thumbnails (e.g., 50GB+ depending on library size).
- Under Options -> Features, enable Nesting. (Leave AppArmor enabled).
2. Configure Host Sub-IDs
On the Proxmox host, allow the root user to delegate UID/GID 2500 to the container:
echo "root:2500:1" >> /etc/subuid
echo "root:2500:1" >> /etc/subgid3. Edit LXC Configuration
On the Proxmox host, open the container configuration file (nano /etc/pve/lxc/<CTID>.conf) and append the ID mapping and bind mount configurations:
# Mount the HDD dataset to the LXC
mp0: /tank/data/photos,mp=/mnt/immich_photos
# Map standard UIDs to the unprivileged range
lxc.idmap: u 0 100000 2500
lxc.idmap: g 0 100000 2500
# Surgically map container UID 2500 to host UID 2500
lxc.idmap: u 2500 2500 1
lxc.idmap: g 2500 2500 1
# Map the rest of the UIDs
lxc.idmap: u 2501 102501 63035
lxc.idmap: g 2501 102501 63035
Reboot the LXC container to apply these mappings.
Log into the LXC container console as root.
1. Install Docker
apt update && apt upgrade -y
apt install curl -y
curl -fsSL https://get.docker.com | shRemain in the LXC container console.
1. Fetch Immich Files
mkdir -p /opt/immich
cd /opt/immich
wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env2. Configure .env
Edit the .env file (nano .env). Update the upload location to point to the ZFS bind mount:
UPLOAD_LOCATION=/mnt/immich_photos
(Also set a secure DB_PASSWORD and match your timezone).
3. Modify docker-compose.yml (The Overlay Architecture)
Edit the docker-compose.yml to enforce the mapped UID and overlay the SSD named volumes on top of the HDD mount.
Apply the following to both the immich-server and immich-machine-learning blocks:
user: "2500:2500"
volumes:
# Base HDD Mount (Handles /library and /upload instantly)
- ${UPLOAD_LOCATION}:/usr/src/app/external
# SSD Named Volume Overlays (Intercepts heavy I/O directories)
- immich_thumbs:/usr/src/app/external/thumbs
- immich_video:/usr/src/app/external/encoded-video
- immich_profile:/usr/src/app/external/profile
- /etc/localtime:/etc/localtime:ro(Leave the postgres, redis, and typesense service blocks exactly as they are).
At the bottom of the file, define the new named volumes:
volumes:
pgdata:
model-cache:
tsdata:
immich_thumbs:
immich_video:
immich_profile:Because Docker creates new named volumes as root, we must initialize them as UID 2500 before starting Immich.
1. Create Volumes without Starting
docker compose up --no-start2. Fix SSD Volume Ownership Run a temporary Alpine container to correctly permission the new SSD volumes:
docker run --rm \
-v immich_thumbs:/thumbs \
-v immich_video:/video \
-v immich_profile:/profile \
alpine chown -R 2500:2500 /thumbs /video /profile3. Launch the Stack
docker compose up -d- Database (PostgreSQL): Running in default Docker named volume on the fast SSD.
- Thumbnails/Transcodes: Running in custom named volumes on the fast SSD.
- Original Photos: Bypassing the SSD and writing straight to the ZFS RAID 10 (
/mnt/immich_photos), cleanly owned by UID2500. - Proxmox Host: Isolated from Docker files/UIDs, maintaining perfect security posture.