Deploying Immich as Your Private Photo Cloud: Complete Docker Setup Guide
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
I got tired of paying Google Photos and Apple iCloud for cloud photo storage. For around $40 per year on a VPS like RackNerd, you can run Immich—a full-featured, Google Photos alternative that keeps all your memories under your control. I've been running it in production for eight months now, and it handles my entire photo library with zero hiccups.
Immich isn't just a photo vault either. It's got machine learning-powered object recognition, smart albums, timeline views, and mobile apps that sync just as smoothly as the commercial clouds. The Docker Compose setup is straightforward if you know the gotchas.
In this tutorial, I'll walk you through deploying Immich on a VPS or homelab with PostgreSQL, Redis, and Caddy reverse proxy—everything you need to access your photos securely from anywhere.
What Makes Immich Worth Self-Hosting
Immich launched in 2022 and has matured rapidly. Unlike some self-hosted photo projects that feel clunky, Immich has a polished web UI and native iOS/Android apps that actually work. The key feature that sold me: machine learning recognition runs locally. Your photos aren't shipped to a cloud classifier; instead, Immich uses CLIP embeddings to search by content without ever leaving your server.
The mobile apps support background sync, so photos upload automatically when you're on Wi-Fi. You get library sharing, smart albums (Immich learned my "food" photos automatically), and a timeline that rivals Google Photos. Plus, you own the hardware—no surprise subscription increases or terms-of-service changes.
On a $40/year VPS (Hetzner or RackNerd both work well), I'm spending less per month than a single cloud subscription.
Prerequisites and Infrastructure Decisions
You'll need a Linux VPS or homelab server with:
- At least 2 CPU cores and 4GB RAM minimum (I use 4 cores, 8GB for comfort)
- 100GB+ storage for photos (scale as needed; I'm at 500GB)
- Docker and Docker Compose installed
- A domain name and SSL certificate (Caddy handles this automatically)
- Outbound HTTPS access (for initial ML model downloads)
I prefer Caddy for reverse proxy duties because it handles Let's Encrypt automatically and requires minimal configuration. Nginx or Traefik work too, but Caddy saved me hours of cert management.
RackNerd and Hetzner both offer affordable VPS in the $40–80/year range. For Immich workloads, I recommend their basic shared CPU plans—no need for expensive RAM because PostgreSQL and Redis stay lean.
Docker Compose Setup: The Complete Stack
I keep my Immich deployment organized in a dedicated directory. Here's my production docker-compose.yml:
version: '3.8'
services:
immich-server:
image: ghcr.io/immich-app/immich-server:release
container_name: immich-server
depends_on:
- immich-postgres
- immich-redis
environment:
DB_HOSTNAME: immich-postgres
DB_USERNAME: immich
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_NAME: immich
REDIS_HOSTNAME: immich-redis
JWT_SECRET: ${JWT_SECRET}
IMMICH_LOG_LEVEL: log
IMMICH_MACHINE_LEARNING_ENABLED: "true"
volumes:
- /mnt/photos/immich:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
ports:
- "3001:3001"
restart: unless-stopped
networks:
- immich-net
immich-microservices:
image: ghcr.io/immich-app/immich-server:release
container_name: immich-microservices
command: start.sh microservices
depends_on:
- immich-postgres
- immich-redis
environment:
DB_HOSTNAME: immich-postgres
DB_USERNAME: immich
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_NAME: immich
REDIS_HOSTNAME: immich-redis
JWT_SECRET: ${JWT_SECRET}
IMMICH_MACHINE_LEARNING_ENABLED: "true"
volumes:
- /mnt/photos/immich:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
restart: unless-stopped
networks:
- immich-net
immich-machine-learning:
image: ghcr.io/immich-app/immich-machine-learning:release
container_name: immich-ml
volumes:
- immich-ml-cache:/cache
environment:
TRANSFORMERS_CACHE: /cache
IMMICH_LOG_LEVEL: log
restart: unless-stopped
networks:
- immich-net
immich-postgres:
image: postgres:16-alpine
container_name: immich-postgres
environment:
POSTGRES_USER: immich
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: immich
volumes:
- immich-db:/var/lib/postgresql/data
restart: unless-stopped
networks:
- immich-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U immich"]
interval: 10s
timeout: 5s
retries: 5
immich-redis:
image: redis:7-alpine
container_name: immich-redis
restart: unless-stopped
networks:
- immich-net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
immich-db:
immich-ml-cache:
networks:
immich-net:
driver: bridge
Save this as docker-compose.yml in your Immich directory. Notice I've split the server and microservices into separate containers—this lets the microservices (background jobs, ML inference) run independently and restart cleanly without disrupting the API.
Environment Variables and Secrets
Create a .env file in the same directory:
POSTGRES_PASSWORD=your_secure_postgres_password_here
JWT_SECRET=$(openssl rand -base64 32)
Generate the JWT_SECRET with that openssl command. Store these in your .env and never commit it to git.
pwgen -s 32 1 (from the pwgen package) to generate strong passwords quickly. It's faster than openssl and produces readable output.
Before deploying, ensure your upload directory exists and has appropriate permissions:
mkdir -p /mnt/photos/immich
chown 1000:1000 /mnt/photos/immich
chmod 755 /mnt/photos/immich
I use /mnt/photos because it's on a separate mount point from the OS disk—better for performance and backup isolation. Adjust the path to your storage setup.
Launching Immich
Navigate to your Immich directory and spin up the stack:
docker-compose up -d
docker-compose logs -f immich-server
Wait 30–60 seconds for PostgreSQL to initialize and Immich to start. The logs will show something like:
immich-server_1 | [Immich] Server is running at http://0.0.0.0:3001
immich-ml_1 | Preloading models...
On first boot, the ML container downloads CLIP models (~500MB). Be patient—this is one-time.
Check container health:
docker-compose ps
All containers should show "running" with no error restarts.
Reverse Proxy with Caddy
I never expose port 3001 directly. Instead, Caddy handles SSL termination and subdomain routing. Here's my Caddyfile:
photos.example.com {
encode gzip
reverse_proxy 127.0.0.1:3001 {
header_uri -Authorization
header_up X-Forwarded-For {http.request.remote.host}
header_up X-Forwarded-Proto {http.request.proto}
}
}
Replace example.com with your domain. Caddy automatically obtains and renews Let's Encrypt certificates. Run it in Docker too:
docker run -d \
--name caddy \
--restart unless-stopped \
-p 80:80 \
-p 443:443 \
-v /etc/caddy/Caddyfile:/etc/caddy/Caddyfile:ro \
-v caddy-data:/data \
-v caddy-config:/config \
caddy:latest
Or add Caddy to your docker-compose.yml for single-file management. DNS for photos.example.com must point to your VPS IP before Caddy can provision the cert.
Initial Configuration and Mobile Apps
Open https://photos.example.com in your browser. Create an admin account. This becomes your primary library owner. The UI is clean—dashboard shows upload stats, recent photos, and smart search.
Download the mobile apps from the App Store (iOS) or Google Play. Log in with your admin credentials. In settings, enable background sync and set the sync frequency. The app uploads photos automatically on Wi-Fi—perfect for hands-off backup.
On first sync, photos get ingested into the database. The ML container classifies them in the background. Expect 24–48 hours for CLIP embeddings to generate if you have a large library (10k+ photos).
Storage and Backup Strategy
By default, photos live in /mnt/photos/immich on your VPS. If your VPS has limited storage, consider:
- Block storage volumes (Hetzner offers cheap block storage, ~$5–10/month per 500GB)
- S3-compatible storage (Immich supports S3 backends; MinIO on your homelab is an option)
- NFS mounts (if you have a NAS in your homelab, mount it to the VPS)
For backups, I use rsync to sync the photo directory to a homelab NAS nightly:
0 2 * * * rsync -av --delete /mnt/photos/immich/ [email protected]:/backup/immich/
Also back up the PostgreSQL database. Add this to your crontab:
0 1 * * * docker exec immich-postgres pg_dump -U immich immich | gzip > /backup/immich-db-$(date +\%Y\%m\%d).sql.gz
Performance Tuning and Scaling
On modest hardware (2GB RAM), Immich runs but gets slow with large libraries. A few tweaks:
- Disable ML if you don't need it: Set
IMMICH_MACHINE_LEARNING_ENABLED: "false"in docker-compose.yml. No object recognition, but uploads are instant. - Increase PostgreSQL shared buffers: Add
shared_buffers=256MBto the Postgres environment if you have spare RAM. - Allocate more CPU to microservices: Use
cpus: "2"in the microservices container to prioritize background jobs. - Consider a separate ML server: Immich can run the ML container on a different machine (e.g., your homelab with a GPU). Set
IMMICH_MACHINE_LEARNING_URL: "http://homelab-ml:3003".
For VPS hosting, RackNerd's annual plans ($40–80/year) are tight but workable. If you hit resource limits, upgrade to a higher tier or migrate to your homelab (Immich runs identically on bare metal or a Proxmox VM).
Common Issues and Troubleshooting
Photos won't upload: Check that /mnt/photos/immich is writable. Run