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/sdbas 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 777command 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
- Open your Portainer instance in a browser (typically
http://[YOUR-PROXMOX-IP]:9000) - Navigate to Stacks in the left sidebar
- Click Add Stack
- Name the stack
immich - 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
- Click Deploy the Stack in Portainer
- Wait for all containers to reach
runningstate — this takes 60 to 90 seconds on first pull - Open your browser and navigate to
http://[YOUR-PROXMOX-IP]:2283 - 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/Photosis mounted into the container as/archive:ro.
The
:roflag 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.
- Navigate to the Immich Web UI at
http://[YOUR-IP]:2283 - Go to Administration in the top navigation
- Select External Libraries from the left sidebar
- Click Create Library
- Set the import path to
/archive(the container-side mount point, not the host path) - 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
- Navigate to Administration in the Immich Web UI
- Select Job Queues
- Confirm the Smart Search (CLIP) job shows as queued or running
- 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— Healthyimmich_microservices— Healthyimmich_redis— Healthyimmich_postgres— Healthy
Connect the Immich Mobile App
- Install the Immich app from the App Store (iOS) or Google Play (Android)
- On the login screen, enter your server URL:
http://[YOUR-IP]:2283 - Log in with the admin credentials you created during setup
- 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
/archiveadded 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_dumpallcommand 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.ymlfor faster video thumbnail generation and streaming. This requires passing the GPU device through to the LXC container. - Automated database backups: Schedule the
pg_dumpallcommand 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
- Immich official documentation: immich.app/docs — the release notes page is essential reading before every update
- r/immich — version-specific issues, community workarounds, and feature discussions
- SelfHostHero: Ultra-Efficient DIY NAS on Proxmox with ZFS + LXC — the companion guide for building the ZFS storage foundation this deployment depends on