Ditch Google Photos Forever: Install Immich on Proxmox with ZFS & Docker

The Privacy Problem With Google Photos

🎧 Listen to the 60-Second Audio Recap:

Google Photos is not a free storage service. It is a data collection engine that happens to store your photos. Every image you upload is analyzed by Google’s machine learning pipeline. Faces are identified and cross-referenced. Objects, locations, and timestamps are extracted. GPS metadata is mapped to build a precise picture of where you live, work, and travel. All of this feeds directly into the advertising profile Google maintains on you.

Immich is the self-hosted alternative. It replicates the Google Photos experience — mobile backup, facial recognition, object search, shared albums, a polished web UI — with one critical difference: every byte of processing happens on your own hardware. The CLIP semantic search model, the face detection pipeline, the GPS indexing — all of it runs locally. Nothing leaves your server.

The self-hosting community verdict is consistent: Immich is not a compromise. It is a genuine replacement. The mobile app is polished, the AI features are competitive, and the project moves fast.

  • Privacy: Google Photos — your images train Google’s ad models. Immich — zero data egress, 100% local AI processing.
  • Cost: Google Photos — free tier ends at 15GB, then $2.99/month per 100GB. Immich — free forever, limited only by your own hardware.
  • Control: Google Photos — Google can change terms, delete accounts, or shut down the service. Immich — you own the data, the database, and the software stack.

What This Guide Covers

This guide walks you through a complete Immich deployment on a Proxmox host. We cover host directory preparation, Docker LXC configuration, and two distinct installation routes. The Newbie Nora route gets you running in under 30 minutes using Portainer. The Pro Paul route builds a production-grade architecture with split storage tiers, read-only archive mounting, and hardened permissions. We also cover storage architecture decisions you must make before you start.

The most common question on r/HomeServer is: “Should I run Immich in an LXC container or a full VM on Proxmox?” The recommendation here is LXC. It has lower overhead, supports direct ZFS dataset passthrough, and is the right choice for a single-purpose Immich deployment. The VM argument is valid if you plan to consolidate multiple Docker workloads on one machine and want stronger isolation between them. For a dedicated photo server, LXC wins.

Decision Framework: Choose LXC if Immich is the primary or only workload in that container. Choose a VM if you are running multiple unrelated Docker stacks and need snapshot-level isolation between them.


Prerequisites & Minimal Hardware

Required Infrastructure

  • Proxmox server with Docker Engine installed inside an LXC container (privileged container required for ZFS bind mounts)
  • Minimum 4 CPU cores and 4GB RAM for basic operation
  • 8GB+ RAM strongly recommended — the CLIP semantic search model and face detection pipeline are memory-hungry during initial scans
  • NVMe storage for the PostgreSQL database, ML model cache, and thumbnail data
  • HDD or ZFS pool for the photo archive — this is your cold storage tier

If you are building your Proxmox storage foundation from scratch, our guide on Ultra-Efficient DIY NAS on Proxmox: Native ZFS + Ubuntu LXC FileServer covers ZFS pool creation, dataset layout, and LXC bind mounts in detail. Read that first if your ZFS pool is not already configured.

Storage Architecture Decision (Make This Choice Before You Start)

Immich writes several distinct types of data with very different performance requirements. The PostgreSQL database needs fast random I/O. The machine learning model cache needs fast sequential reads on startup. Thumbnails are read constantly by the web UI. These are your hot data workloads — they belong on NVMe.

Your actual photo files are cold data. They are written once during upload and read occasionally for viewing. These belong on your HDD or ZFS pool. Mixing hot and cold data on the same disk is the single most common performance mistake in home Immich deployments.


+---------------------------+       +---------------------------+
|        NVMe (Fast)        |       |       HDD / ZFS Pool      |
|---------------------------|       |---------------------------|
| PostgreSQL Database       |       | New Mobile Uploads        |
| ML Model Cache            |       | Existing Photo Archive    |
| Thumbnail Cache           |       | (mounted read-only)       |
|                           |       |                           |
| /mnt/snelle_data/         |       | /mnt/opslag/              |
+---------------------------+       +---------------------------+

ZFS Mirror for Your Archive? If your archive is between 2TB and 50TB, the r/homelab consensus is a ZFS mirror (2-wide) for pure redundancy, or RAIDZ1 for three or more drives when you need capacity. Do not run a single-disk ZFS pool for irreplaceable photos. Use zpool create tank mirror /dev/sda /dev/sdb as your baseline.


Method 1 — The Quick Start (Newbie Nora Route)

Step 1 — Create Host Directories on Proxmox

Directory creation happens on the Proxmox host before the container starts because Docker bind mounts require the source path to exist on the host filesystem. Run these commands directly on your Proxmox node via SSH or the shell console.

mkdir -p /hdd-pool/Opslag/ImmichUploads
chmod -R 777 /hdd-pool/Opslag/ImmichUploads

Warning: The chmod 777 command grants full read/write/execute permissions to all users. This is acceptable for getting Immich running quickly on a trusted home network. It is not appropriate for a hardened production deployment. The Pro Paul route below uses proper UID/GID mapping instead.

Step 2 — Open Portainer and Create a New Stack

  1. Open your Portainer instance in a browser (typically http://[YOUR-PROXMOX-IP]:9000)
  2. Navigate to Stacks in the left sidebar
  3. Click Add Stack
  4. Name the stack immich
  5. Select Web editor as the build method

Step 3 — Paste the Docker Compose YAML

Paste the following into the Portainer web editor. This is the minimal working configuration. All four required services are included.

version: "3.8"

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    volumes:
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    ports:
      - "2283:3001"
    depends_on:
      - redis
      - database
    restart: unless-stopped

  immich-microservices:
    container_name: immich_microservices
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    command: ["start.sh", "microservices"]
    volumes:
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    depends_on:
      - redis
      - database
    restart: unless-stopped

  immich-machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    volumes:
      - model-cache:/cache
    env_file:
      - .env
    restart: unless-stopped

  redis:
    container_name: immich_redis
    image: redis:6.2-alpine
    restart: unless-stopped

  database:
    container_name: immich_postgres
    image: tensorchord/pgvecto-rs:pg14-v0.2.0
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: postgres
      POSTGRES_DB: immich
    volumes:
      - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  model-cache:

Now create the companion .env file. In Portainer, you can add this as a separate environment file or paste the variables directly into the environment variables section.

# Immich version — "release" always pulls the latest stable build
IMMICH_VERSION=release

# Path where new photo uploads will be stored (on your HDD)
UPLOAD_LOCATION=/hdd-pool/Opslag/ImmichUploads

# Path for the PostgreSQL database (use NVMe if available)
DB_DATA_LOCATION=/hdd-pool/Opslag/ImmichDB

# Strong password for the database — change this before deploying
DB_PASSWORD=change_this_to_a_strong_password

# Database connection string — do not modify this line
DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@database:5432/immich

# Redis connection string — do not modify this line
REDIS_HOSTNAME=redis

Step 4 — Deploy and Access the Web UI

  1. Click Deploy the Stack in Portainer
  2. Wait for all containers to reach running state — this takes 60 to 90 seconds on first pull
  3. Open your browser and navigate to http://[YOUR-PROXMOX-IP]:2283
  4. Complete the admin account creation wizard — set a strong password here

You now have a working Immich instance. The mobile app can connect immediately. Continue to the Configuration and Validation section to confirm everything is healthy.


Method 2 — The Pro Setup (Pro Paul Route)

The Data-Splitting Architecture — Why It Matters

The quick start route works, but it puts everything on one storage path. The pro architecture separates your data into three distinct zones, each matched to the right storage medium.

  • Zone 1 — NVMe (Hot): PostgreSQL database and ML model cache live at /mnt/snelle_data/App_Data/. These need the fastest possible I/O.
  • Zone 2 — HDD (Write): New mobile uploads land at /mnt/opslag/ImmichUploads. Write speed here is acceptable on spinning disk.
  • Zone 3 — HDD (Read-Only): Your existing curated photo archive at /mnt/opslag/Photos is mounted into the container as /archive:ro.

The :ro flag is non-negotiable. Mounting your existing archive as read-only prevents Immich from renaming, moving, or modifying any file in your carefully organized collection. Immich indexes the files and builds its database from them, but it cannot touch the originals. Do not skip this.

Advanced Docker Compose YAML — Full Configuration

version: "3.8"

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    volumes:
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
      - /mnt/opslag/Photos:/archive:ro
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    ports:
      - "2283:3001"
    depends_on:
      - redis
      - database
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 2G

  immich-microservices:
    container_name: immich_microservices
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    command: ["start.sh", "microservices"]
    volumes:
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
      - /mnt/opslag/Photos:/archive:ro
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    depends_on:
      - redis
      - database
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "4.0"
          memory: 4G

  immich-machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    volumes:
      - ${ML_CACHE_LOCATION}:/cache
    env_file:
      - .env
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 2G

  redis:
    container_name: immich_redis
    image: redis:6.2-alpine
    restart: unless-stopped

  database:
    container_name: immich_postgres
    image: tensorchord/pgvecto-rs:pg14-v0.2.0
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: postgres
      POSTGRES_DB: immich
    volumes:
      - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 1G

The production .env file for this configuration:

# Immich version
IMMICH_VERSION=release

# Zone 2: New mobile uploads — HDD path
UPLOAD_LOCATION=/mnt/opslag/ImmichUploads

# Zone 1: PostgreSQL database — NVMe path for fast I/O
DB_DATA_LOCATION=/mnt/snelle_data/App_Data/ImmichDB

# Zone 1: ML model cache — NVMe path
ML_CACHE_LOCATION=/mnt/snelle_data/App_Data/ImmichMLCache

# Database password — use a password manager to generate this
DB_PASSWORD=change_this_to_a_strong_password

# Database connection string — do not modify
DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@database:5432/immich

# Redis connection string — do not modify
REDIS_HOSTNAME=redis

Configuring the External Library (Archive Mount)

Once the stack is deployed and all containers are healthy, you need to register your read-only archive with Immich so it can index the existing files.

  1. Navigate to the Immich Web UI at http://[YOUR-IP]:2283
  2. Go to Administration in the top navigation
  3. Select External Libraries from the left sidebar
  4. Click Create Library
  5. Set the import path to /archive (the container-side mount point, not the host path)
  6. Click Scan Library to begin indexing

Warning: Never set the External Library import path to /usr/src/app/upload. This is the main upload directory and Immich will reject it with an “Invalid import path” error. Always use a dedicated, separately mounted path like /archive. See the Troubleshooting section if you hit this error.

Verifying AI Model Activation

  1. Navigate to Administration in the Immich Web UI
  2. Select Job Queues
  3. Confirm the Smart Search (CLIP) job shows as queued or running
  4. Confirm the Face Detection job shows as queued or running

You can also verify the ML container is loading its models correctly from the command line:

docker logs immich_machine_learning --tail 50

You should see log lines indicating the CLIP model and facial recognition model are being loaded from the cache directory. If the container is restarting or showing errors, check that your ML_CACHE_LOCATION path exists and is writable.


Configuration & Validation

Container Health Check via Portainer

All four core containers must report a healthy status before you consider the deployment complete. Check this in Portainer under your immich stack, or run the following command on the Proxmox host:

docker ps --format "table {{.Names}}\t{{.Status}}"

Expected output should show all containers as Up X minutes (healthy):

  • immich_server — Healthy
  • immich_microservices — Healthy
  • immich_redis — Healthy
  • immich_postgres — Healthy

Connect the Immich Mobile App

  1. Install the Immich app from the App Store (iOS) or Google Play (Android)
  2. On the login screen, enter your server URL: http://[YOUR-IP]:2283
  3. Log in with the admin credentials you created during setup
  4. Navigate to Profile then Backup and enable automatic backup

Note: Automatic backup only triggers on Wi-Fi by default. Verify this setting before you leave your home network for the first time. You do not want to discover your backup has not run in three weeks because you were traveling.

Final Validation Checklist

  • All 4 Docker containers report Healthy
  • Admin account created and accessible via web UI
  • External Library /archive added and scan initiated
  • Job Queues show CLIP and Face Detection activity
  • Mobile app connected with auto-backup enabled

The Ugly Truth — Quirks and Honest Expectations

The First Scan Will Destroy Your CPU (Temporarily)

This is not an exaggeration. When Immich processes a large archive for the first time, it runs every photo through the CLIP embedding model and the face detection pipeline. On a 1TB archive of roughly 100,000 photos, this process can take two to four days on a mid-range home server. During that time, CPU utilization will sit at or near 100% on every core assigned to the immich_microservices container. If your Proxmox host is also running other services, this will impact them.

Practical advice: Start the initial scan overnight or over a weekend. If you used the Pro route, the CPU limits in your Docker Compose file will cap the microservices container and protect your other workloads. If you used the Newbie route, consider adding resource limits before triggering a large archive scan. Thermal management matters here — ensure your server has adequate airflow during the initial processing period.

Immich Is Still “Active Development” Software

The Immich developers are explicit about this in their own documentation: do not treat this as enterprise-stable software. Breaking changes between versions do occur. Database schema migrations occasionally require manual intervention. This does not mean you should not use it — it means you must have a backup discipline before every update.

Run this command before every docker compose pull:

docker exec -t immich_postgres pg_dumpall -c -U postgres > immich_backup_$(date +%Y%m%d).sql

Three non-negotiable backup practices before any Immich update:

  • Run the pg_dumpall command above and verify the output file is non-zero in size
  • Copy the backup SQL file off the Proxmox host to a separate machine or NAS
  • Read the Immich release notes on GitHub before pulling — look for any entries marked “breaking change” or “migration required”

Troubleshooting Common Errors

Error — “Invalid import path” on External Library

Symptom: The External Library scan fails immediately after clicking Scan, with a path validation error in the UI or in the immich_server logs.

Root Cause: The import path is set to /usr/src/app/upload, which is the main Immich upload directory. Immich explicitly blocks this path from being used as an external library to prevent circular references.

Fix: In your docker-compose.yml, add a separate volume mount for your archive with a distinct container-side path. Then use that path in the External Libraries UI.

volumes:
  - /mnt/opslag/Photos:/archive:ro

Set the External Library import path to /archive, not to any subdirectory of /usr/src/app/upload.

Error — Crash Loop with CPU Spikes Every 20 Seconds

Symptom: The immich_server or immich_microservices container restarts repeatedly. Each restart cycle produces a brief CPU spike visible in Portainer or htop.

Root Cause A — Permissions error: The upload directory on the host is not writable by the container process. This produces an EACCES error in the container logs.

Fix A:

chmod 777 /hdd-pool/Opslag/ImmichUploads

Root Cause B — Corrupted database initialization: The PostgreSQL data directory was partially initialized during a previous failed startup attempt. The database cannot start cleanly from a partial state.

Fix B: Stop all containers, delete the contents of the DB_DATA_LOCATION directory on your NVMe path, then restart the stack. Docker will reinitialize a clean PostgreSQL instance. You will lose any existing Immich data, so only do this on a fresh deployment.

docker compose down
rm -rf /mnt/snelle_data/App_Data/ImmichDB/*
docker compose up -d

Error — LXC Container Cannot Access ZFS Mount Points

Symptom: All containers start without errors, but any write operation to the HDD paths fails with “Permission denied” in the Immich logs. The paths exist on the host but are inaccessible from inside the LXC container.

Root Cause: The ZFS dataset is mounted on the Proxmox host but has not been bound into the LXC container configuration. The container cannot see host mount points unless they are explicitly passed through in the Proxmox LXC config file.

Fix: Add a mount point entry to the LXC configuration file on the Proxmox host. Replace [CTID] with your container ID number.

# Edit the LXC config file at /etc/pve/lxc/[CTID].conf
# Add the following line:
mp0: /mnt/opslag,mp=/mnt/opslag

If you need to pass through multiple paths, increment the mount point index: mp0, mp1, mp2, and so on. Restart the LXC container after saving the config file.

This is the most frequently missed step in community Immich guides. Most tutorials cover the Docker Compose configuration in detail but skip the Proxmox-side bind mount entirely. If your containers are running but cannot write to your ZFS datasets, this is almost certainly the cause.


Conclusion & Next Steps

What You’ve Built

  • Immich running stable in Docker on a Proxmox LXC container
  • AI models processing faces and objects 100% locally — no cloud dependency
  • Existing photo archive mounted read-only and safely indexed
  • Mobile app connected with automatic backup active
  • Database backup procedure documented and ready to schedule

You now have a self-hosted photo management platform that matches Google Photos feature-for-feature, runs entirely on your own hardware, and costs nothing beyond the electricity to run it. Your photos are yours.

Where to Go From Here

  • Multi-user setup: Create individual accounts for family members under Administration. Each user gets their own library, and you can create shared albums across accounts.
  • Hardware transcoding: Enable Intel QuickSync or NVENC in your docker-compose.yml for faster video thumbnail generation and streaming. This requires passing the GPU device through to the LXC container.
  • Automated database backups: Schedule the pg_dumpall command via cron on the Proxmox host. A daily backup job that copies the SQL dump to a separate NAS or offsite location is the minimum acceptable backup posture for irreplaceable photos.
  • Reverse proxy with HTTPS: Put Nginx Proxy Manager or Caddy in front of Immich so you can access your photos securely from outside your home network without exposing port 2283 directly.

Community Resources