Skip to content

Instantly share code, notes, and snippets.

@potat-dev
Last active May 17, 2026 11:25
Show Gist options
  • Select an option

  • Save potat-dev/b8f055bf225e5843c078bd16aa5173bd to your computer and use it in GitHub Desktop.

Select an option

Save potat-dev/b8f055bf225e5843c078bd16aa5173bd to your computer and use it in GitHub Desktop.
Immich LXC ZFS mount configuration

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.


Phase 1: Proxmox Host Preparation (Storage & Security)

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_storage

2. 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/photos

3. Apply Permissions

chown 2500:2500 /tank/data/photos
chmod 750 /tank/data/photos

Phase 2: LXC Creation & IDMAP Configuration

1. 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/subgid

3. 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.


Phase 3: Docker Setup

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 | sh

Phase 4: Immich Configuration

Remain 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.env

2. 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:

Phase 5: Initialization & Spin-Up

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-start

2. 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 /profile

3. Launch the Stack

docker compose up -d

Final Architecture Verification

  • 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 UID 2500.
  • Proxmox Host: Isolated from Docker files/UIDs, maintaining perfect security posture.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment