LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Immich Deep Dive: AI-Powered Photo Management for Your Homelab

homelabself-hostingimmichphotosdockeraimachine-learning

Your phone has 30,000 photos. Google Photos holds them hostage behind an ever-shrinking free tier. Apple iCloud costs $2.99/month and makes sharing difficult. You want your memories on hardware you own, with search that actually works, face recognition that groups your family together automatically, and mobile backup that just runs.

Immich is that solution. It’s the self-hosted photo management app that has done for photos what Jellyfin did for video — built a genuinely good, open-source alternative to the cloud giants. With AI-powered semantic search, DBSCAN-based face clustering, hardware-accelerated ML, and a polished mobile app, Immich is no longer a compromise. For many people it’s better than Google Photos.

This guide covers everything: architecture, Docker Compose deployment, hardware acceleration, face recognition internals, mobile backup, storage design for large libraries, and migration from Google Photos.


Architecture Overview

Immich follows a microservices pattern with four core backend components communicating over Docker networks:

┌─────────────────────────────────────────────────────────────┐
│                         Clients                              │
│  Mobile (Flutter/Dart)  │  Web (SvelteKit)  │  CLI (Node)   │
└───────────────────────────────┬─────────────────────────────┘
                                │ REST/OpenAPI
┌───────────────────────────────▼─────────────────────────────┐
│                      immich-server                           │
│   NestJS · REST API · Job scheduling · Admin panel          │
└──────┬─────────────────────────────────┬────────────────────┘
       │ BullMQ jobs                     │ ML inference
┌──────▼───────────┐            ┌────────▼──────────────────┐
│ immich-microsvcs │            │  immich-machine-learning  │
│ Background jobs  │            │  FastAPI · ONNX models    │
│ Thumbnail gen    │            │  CLIP · InsightFace       │
│ EXIF extraction  │            │  CUDA/OpenVINO/ROCm/RKNN  │
│ Transcoding      │            └───────────────────────────┘
└──────┬───────────┘
       │
┌──────▼──────────────────────────┐   ┌──────────────┐
│        PostgreSQL + pgvector     │   │    Redis      │
│  Assets · People · Albums ·     │   │  (Valkey 9)   │
│  CLIP vectors · face embeddings │   │  BullMQ queue │
└─────────────────────────────────┘   └──────────────┘

immich-server handles all HTTP traffic. It also schedules background jobs into Redis queues — but does not execute them.

immich-microservices is a separate container running the same image with a different entrypoint. It consumes jobs from Redis: thumbnail generation, EXIF extraction, video transcoding, and triggering ML jobs. Because jobs fan out across the microservices workers, you can scale this container independently.

immich-machine-learning is a Python FastAPI service. It runs ONNX-format models for two tasks: CLIP-based semantic search and InsightFace-based facial recognition. Models are cached in a Docker volume and downloaded on first use.

PostgreSQL stores everything — assets, albums, people, shared links, tags — plus CLIP vector embeddings and facial recognition embeddings stored as pgvector columns. Semantic search queries translate directly into vector similarity searches.

Redis (Valkey) is the job queue. Every background task flows through BullMQ: when a new photo uploads, the server enqueues a thumbnail job; when thumbnails complete, a facial recognition job is enqueued, and so on. Redis also provides session caching.

The client layers — iOS/Android apps written in Flutter/Dart, web app in SvelteKit with Tailwind CSS, and the Node.js CLI — all speak to the server via auto-generated OpenAPI clients.


Docker Compose Deployment

Directory and environment setup

1
mkdir -p /opt/immich && cd /opt/immich

Create .env:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# .env
IMMICH_VERSION=release

# Where photos are stored — use a large disk
UPLOAD_LOCATION=/mnt/storage/immich/library

# Database data — use an SSD
DB_DATA_LOCATION=/mnt/ssd/immich/postgres

DB_PASSWORD=change_me_use_a_long_random_string
DB_USERNAME=postgres
DB_NAME=immich

TZ=America/New_York

Full docker-compose.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
services:
  immich-server:
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    ports:
      - "2283:3001"
    environment:
      DB_HOSTNAME: database
      DB_PORT: 5432
      DB_USERNAME: ${DB_USERNAME}
      DB_PASSWORD: ${DB_PASSWORD}
      DB_DATABASE_NAME: ${DB_NAME}
      REDIS_HOSTNAME: redis
      UPLOAD_LOCATION: /usr/src/app/upload
      TZ: ${TZ}
    volumes:
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
    depends_on:
      database:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3001/api/server/ping"]
      interval: 30s
      timeout: 10s
      retries: 5

  immich-microservices:
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    command: start-microservices
    environment:
      DB_HOSTNAME: database
      DB_PORT: 5432
      DB_USERNAME: ${DB_USERNAME}
      DB_PASSWORD: ${DB_PASSWORD}
      DB_DATABASE_NAME: ${DB_NAME}
      REDIS_HOSTNAME: redis
      UPLOAD_LOCATION: /usr/src/app/upload
      TZ: ${TZ}
    volumes:
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
    depends_on:
      database:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  immich-machine-learning:
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    environment:
      MACHINE_LEARNING_DEVICE: cpu    # Change to: cuda, rocm, openvino
      MACHINE_LEARNING_CACHE_FOLDER: /cache
      MACHINE_LEARNING_WORKERS: 1
      MACHINE_LEARNING_REQUEST_TIMEOUT: 120
    volumes:
      - model-cache:/cache
    restart: unless-stopped

  redis:
    image: valkey/valkey:9-alpine
    command: valkey-server --appendonly yes
    volumes:
      - redis-data:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10

  database:
    image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
    environment:
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_INITDB_ARGS: >-
        --encoding=UTF8
        --lc-collate=C
        --lc-ctype=C
    volumes:
      - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  model-cache:
  redis-data:

Start everything:

1
2
docker compose up -d
docker compose logs -f immich-server

Navigate to http://your-server:2283 and create the admin account. First user becomes the administrator.

Important: Always use docker compose (not docker-compose). Environment variable changes require docker compose up -d --force-recreate — a plain restart does not apply new variables.

Putting Immich behind Traefik

Add labels to immich-server:

1
2
3
4
5
6
7
  immich-server:
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.immich.rule=Host(`photos.yourdomain.com`)"
      - "traefik.http.routers.immich.entrypoints=websecure"
      - "traefik.http.routers.immich.tls.certresolver=letsencrypt"
      - "traefik.http.services.immich.loadbalancer.server.port=3001"

Hardware Acceleration for Machine Learning

The ML container runs two models continuously: CLIP for semantic search embeddings, and InsightFace for facial recognition. On CPU, these are slow. With a GPU or dedicated accelerator they become near-instant.

CUDA (NVIDIA)

Requires NVIDIA Container Toolkit and driver >= 545 (CUDA 12.3+).

1
2
3
4
5
6
7
# Install NVIDIA Container Toolkit
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | \
  sudo tee /etc/apt/sources.list.d/nvidia-docker.list
sudo apt update && sudo apt install -y nvidia-container-toolkit
sudo systemctl restart docker

Update docker-compose.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  immich-machine-learning:
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}-cuda
    environment:
      MACHINE_LEARNING_DEVICE: cuda
      MACHINE_LEARNING_CACHE_FOLDER: /cache
    volumes:
      - model-cache:/cache
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    restart: unless-stopped

OpenVINO (Intel integrated/arc GPU)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  immich-machine-learning:
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}-openvino
    environment:
      MACHINE_LEARNING_DEVICE: openvino
    devices:
      - /dev/dri:/dev/dri
    group_add:
      - video
    volumes:
      - model-cache:/cache
    restart: unless-stopped

OpenVINO uses more RAM than CPU mode but is significantly faster. Requires a recent kernel for device support.

ROCm (AMD)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  immich-machine-learning:
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}-rocm
    environment:
      MACHINE_LEARNING_DEVICE: rocm
    devices:
      - /dev/kfd:/dev/kfd
      - /dev/dri:/dev/dri
    group_add:
      - render
      - video
    security_opt:
      - seccomp=unconfined
    volumes:
      - model-cache:/cache
    restart: unless-stopped

Note: The ROCm image is large (~35 GiB). Ensure sufficient disk space before pulling.

Remote ML server

If your photo server is low-powered (Raspberry Pi, old NAS), you can offload ML to a separate machine:

1
2
3
  immich-server:
    environment:
      MACHINE_LEARNING_URL: http://192.168.1.100:3003

Run the ML container on your GPU machine with port 3003 exposed. The Immich server sends inference requests over the network.


How Face Recognition Works

Understanding the internals helps you tune it for your library.

The pipeline

  1. Upload triggers thumbnail generation in immich-microservices
  2. Thumbnail completion enqueues a facial recognition job
  3. Face detection (InsightFace detection model): finds bounding boxes and confidence scores for faces in each photo
  4. Embedding generation (InsightFace recognition model): crops each face and generates a 512-dimensional embedding vector
  5. Embeddings stored in PostgreSQL alongside the asset
  6. DBSCAN clustering groups faces with similar embeddings into “People”

DBSCAN clustering

Immich uses a modified DBSCAN (Density-Based Spatial Clustering of Applications with Noise) algorithm. Unlike k-means, DBSCAN doesn’t require knowing the number of clusters upfront — critical since you don’t know how many distinct people are in your library.

DBSCAN classifies points as:

  • Core points: Have at least minSamples neighbors within distance epsilon
  • Border points: Within epsilon of a core point but not dense enough to be core
  • Noise: Not reachable from any core point (unrecognized faces)

Immich’s implementation is incremental — new faces are assigned to existing clusters rather than requiring full reclustering. A new face joins an existing cluster if it falls within epsilon of a core point in that cluster. This means face recognition improves over time as more photos are processed.

Tuning parameters (Admin > Administration > Machine Learning):

Setting Default Effect
Facial Recognition Model buffalo_l Accuracy/speed tradeoff
Min Detection Score 0.7 Higher = fewer false detections
Min Recognized Faces 3 Minimum faces to create a named person
Max Recognition Distance 0.6 Lower = stricter matching

Accuracy in practice

Expect 80–95% accuracy for typical household libraries:

  • Works best with clear, frontal, well-lit faces
  • Degrades with occlusion, extreme angles, infant photos (faces change rapidly)
  • Improves significantly as you assign names — tagged faces become reference anchors

Strategy for large imports:

1. Import everything
2. Set "Min Recognized Faces" = 20
3. Run "Recognize Faces" on ALL assets
4. Name the clearly-clustered people
5. Lower threshold to 10, re-run in MISSING mode
6. Name any new clusters
7. Lower to 5, re-run in MISSING mode
8. Merge similar unnamed clusters

This incremental approach prevents the “flood of micro-clusters” problem that occurs when you start with a low threshold on a fresh import.


Semantic Search with CLIP

Immich’s “Search” isn’t just filename matching — it’s semantic vector search powered by CLIP (Contrastive Language-Image Pretraining).

When you upload a photo, the ML service generates a CLIP embedding — a vector representation of the image’s semantic content. These vectors are stored in PostgreSQL via pgvector. When you search “dog at beach”, your query is also converted to a CLIP vector and PostgreSQL finds the nearest image vectors using cosine similarity.

This means you can search:

  • "sunset over mountains" — finds photos by visual content
  • "birthday cake" — finds birthday celebrations
  • "sleeping baby" — finds sleeping infants
  • "documents" — finds photos of papers, whiteboards

No tags, no captions required. The model understands the relationship between language and visual content.

CLIP vectors are also used for “More like this” — finding visually similar photos to a selected image.


Mobile Backup Configuration

Android

  1. Install Immich app from F-Droid, Google Play, or direct APK
  2. Open app → tap the user icon → Settings
  3. Enable Background backup
  4. Grant photo permissions (All Photos, Videos)
  5. Configure which albums to back up (default: Camera, Screenshots)
  6. Set backup timing: While charging only (recommended for large initial syncs)

Battery optimization warning: Samsung, Xiaomi, OnePlus, and Huawei devices aggressively kill background apps. Add Immich to your battery optimization exclusion list:

  • Samsung: Settings → Device care → Battery → Background usage limits → Never sleeping apps → Add Immich
  • Xiaomi: Settings → Apps → Immich → Battery saver → No restrictions
  • OnePlus: Settings → Battery → Battery optimization → Immich → Don’t optimize

Check dontkillmyapp.com for device-specific instructions.

iOS

  1. Install Immich from the App Store
  2. Settings → General → Background App Refresh → Enable for Immich
  3. Disable Low Power Mode for reliable background sync
  4. Open Immich → Profile → Settings → Background backup → Enable

iOS background sync frequency is entirely controlled by iOS. Apple’s algorithm considers: how often you open the app, device battery level, network conditions, time of day. Apps you use frequently get more background time. There’s no way to force immediate background sync — opening the app triggers foreground backup.

Connection settings

Server URL: https://photos.yourdomain.com
# Or local: http://192.168.1.100:2283

For secure external access without exposing ports directly, use Cloudflare Tunnel:

1
2
3
4
5
# cloudflared tunnel configuration
ingress:
  - hostname: photos.yourdomain.com
    service: http://immich-server:3001
  - service: http_status:404

Storage Design for Large Libraries

This is the single highest-impact decision you’ll make. Getting storage layout right from the start prevents painful migrations later.

The naive setup (avoid)

1
2
volumes:
  - /mnt/hdd/immich:/usr/src/app/upload  # Everything on the same HDD

This works for small libraries. For 50k+ photos, it’s painful. Immich generates enormous numbers of small files — multiple thumbnail sizes for every photo, encoded video variants, database files. Small files on spinning disks are slow due to seek time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  immich-server:
    volumes:
      # Original photos/videos — large HDD array
      - /mnt/hdd/immich/library:/usr/src/app/upload/library
      - /mnt/hdd/immich/upload:/usr/src/app/upload/upload
      # Generated content — fast SSD
      - /mnt/ssd/immich/thumbs:/usr/src/app/upload/thumbs
      - /mnt/ssd/immich/encoded-video:/usr/src/app/upload/encoded-video
      - /mnt/ssd/immich/profile:/usr/src/app/upload/profile
      # Database — NVMe if possible
      # (configured via DB_DATA_LOCATION in .env)

  database:
    volumes:
      - /mnt/nvme/immich/postgres:/var/lib/postgresql/data

Your originals sit on cheap HDD capacity. Thumbnails (which the web app loads constantly while scrolling) live on SSD. The database, which services every API call, lives on the fastest storage you have.

Storage template

The storage template controls how Immich organizes uploaded files on disk. Configure at Admin → Administration → Settings → Storage Template.

Default: upload/YYYY/MM/DD/filename.jpg

Custom examples:
{albumName}/{YYYY}/{MM}/{originalFilename}    # Album-organized
{person}/{YYYY}/{originalFilename}            # Person-organized (after tagging)
{YYYY}/{MM}/{DD}/{deviceName}/{originalFilename}  # Date + device

Template variables: {YYYY}, {MM}, {DD}, {HH}, {mm}, {ss}, {originalFilename}, {fileExtension}, {albumName}, {person}, {deviceName}, {assetId}.

After changing the template, run “Storage Migration” job under Admin → Jobs to reorganize existing files.

External libraries

External libraries let Immich index photos that live outside its upload directory — useful if you have an existing NAS library you don’t want to move.

  1. Admin → External Libraries → Create Library
  2. Set import paths: /mnt/nas/photos
  3. Schedule automatic scanning or trigger manually
  4. Immich indexes files without moving them; originals stay in place

External library files cannot be uploaded to from the mobile app — external libraries are read-only from Immich’s perspective.


Backup Strategy

Immich stores two distinct things you need to back up:

  1. PostgreSQL database — asset metadata, albums, people, shares, face embeddings, CLIP vectors
  2. Asset files — original photos, videos, thumbnails, encoded video

The built-in database backup runs daily at 2:00 AM and stores dumps in UPLOAD_LOCATION/backups/. By default the last 14 dumps are retained.

Consistent backup procedure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env bash
# immich-backup.sh — run via cron

set -euo pipefail

BACKUP_DEST="/mnt/backup/immich"
IMMICH_DIR="/opt/immich"
UPLOAD_LOCATION="/mnt/storage/immich/library"
DATE=$(date +%Y%m%d_%H%M%S)

# Stop server to prevent in-flight writes
cd "$IMMICH_DIR"
docker compose stop immich-server immich-microservices

# Backup database
docker compose exec database pg_dumpall -U postgres | \
  gzip > "${BACKUP_DEST}/db_${DATE}.sql.gz"

# Sync assets (rsync is incremental — only copies changes)
rsync -av --delete \
  "${UPLOAD_LOCATION}/" \
  "${BACKUP_DEST}/library/"

# Restart services
docker compose start immich-server immich-microservices

echo "Backup complete: ${DATE}"

For offsite backup, pipe to Restic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Install restic
sudo apt install restic

# Initialize B2 repository
restic -r b2:your-bucket-name:immich init

# Backup (add to cron after stopping containers)
restic -r b2:your-bucket-name:immich backup \
  /mnt/storage/immich/library \
  /mnt/ssd/immich \
  --exclude "*.tmp" \
  --tag immich

# Verify latest snapshot integrity
restic -r b2:your-bucket-name:immich check

Restore procedure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 1. Stop Immich
docker compose down

# 2. Drop and recreate database
docker compose up -d database
sleep 10
docker compose exec database psql -U postgres -c "DROP DATABASE immich;"
docker compose exec database psql -U postgres -c "CREATE DATABASE immich;"

# 3. Restore database dump
gunzip -c /mnt/backup/immich/db_20260327_020000.sql.gz | \
  docker compose exec -T database psql -U postgres

# 4. Restore asset files
rsync -av /mnt/backup/immich/library/ /mnt/storage/immich/library/

# 5. Start everything
docker compose up -d

Migrating from Google Photos

Export with Google Takeout

  1. Go to takeout.google.com
  2. Deselect all, then select Google Photos only
  3. Choose “All photo albums included”
  4. Export format: ZIP, maximum file size (50 GB recommended)
  5. Download all ZIP archives

Takeout produces JSON sidecar files alongside each photo containing Google’s metadata (date taken, GPS coordinates, starred status, album membership).

Import with immich-go

The official @immich/cli can upload files but doesn’t handle Takeout’s JSON sidecars correctly. immich-go is the purpose-built migration tool:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Download immich-go
curl -L https://github.com/simulot/immich-go/releases/latest/download/immich-go_Linux_x86_64.tar.gz | tar xz
chmod +x immich-go

# Get your API key: Immich web → Profile → API Keys → New key

# Dry run first — see what will be uploaded without actually doing it
./immich-go upload from-google-photos \
  --server=http://192.168.1.100:2283 \
  --api-key=your-api-key-here \
  --dry-run \
  ~/takeout-*.zip

# Actual migration
./immich-go upload from-google-photos \
  --server=http://192.168.1.100:2283 \
  --api-key=your-api-key-here \
  ~/takeout-*.zip

immich-go handles:

  • JSON sidecar metadata (date taken, GPS, description)
  • Duplicate detection via file hashing (typically ~33% of exports are duplicates)
  • RAW + JPEG pairs (kept together, not double-counted)
  • Album reconstruction
  • Starred photos (mapped to Immich favorites)

Expect a large initial migration to take several hours to days depending on library size and server hardware.


OAuth / OIDC Integration

With Authentik

Create an OAuth2/OpenID Provider in Authentik:

  • Redirect URIs: https://photos.yourdomain.com/auth/callback
  • Also add mobile: app.immich:///oauth-callback
  • Signing Key: your Authentik signing key
  • Note the Client ID and Client Secret

In Immich (Admin → Administration → Settings → OAuth):

Issuer URL:     https://auth.yourdomain.com/application/o/immich/
Client ID:      <from Authentik>
Client Secret:  <from Authentik>
Scope:          openid profile email
Button Text:    Sign in with Authentik
Auto Launch:    enabled (skips local login form)

If you lock yourself out after enabling Auto Launch, navigate to /auth/login?autoLaunch=0 to access the local login form.

With Authelia, Keycloak, or any OIDC provider

The configuration is provider-agnostic — set the Issuer URL to your OIDC provider’s discovery endpoint and provide Client ID/Secret. Immich auto-discovers the token endpoint, userinfo endpoint, and signing keys from the OIDC discovery document.


CLI Usage

The official CLI is useful for scripted uploads and automation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
npm install -g @immich/cli

# Login (stores token in ~/.config/immich/)
immich login-key --api-key YOUR_KEY --instance-url http://192.168.1.100:2283

# Upload a directory, creating albums from folder names
immich upload --recursive --album /mnt/nas/sorted-photos/

# Dry run — preview what would be uploaded
immich upload --dry-run --recursive /path/to/photos/

# Upload and delete locals after successful transfer
immich upload --delete-local --recursive /incoming/

# Skip hashing (faster but allows duplicates)
immich upload --skip-hash --recursive /path/to/photos/

# Watch directory continuously
immich upload --watch /mnt/incoming/

# Show server info
immich server-info

For Docker environments without Node.js installed:

1
2
3
4
5
6
docker run --rm \
  -e IMMICH_INSTANCE_URL=http://192.168.1.100:2283 \
  -e IMMICH_API_KEY=your-api-key \
  -v /mnt/photos:/photos \
  ghcr.io/immich-app/immich-cli:latest \
  upload --recursive /photos

Performance Tuning for Large Libraries

Hardware requirements by library size

Library Size CPU RAM SSD (DB + thumbs) HDD (originals)
< 10k photos 2 cores 4 GB 50 GB 500 GB
10k–50k 4 cores 8 GB 200 GB 2 TB
50k–200k 8 cores 16 GB 500 GB 8 TB
200k+ 16+ cores 32+ GB 1+ TB NVMe 20+ TB

PostgreSQL tuning

For large libraries, increase shared_buffers in the database container:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  database:
    image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
    environment:
      POSTGRES_INITDB_ARGS: >-
        --encoding=UTF8
        --lc-collate=C
        --lc-ctype=C
    command: >
      postgres
      -c shared_buffers=1GB
      -c effective_cache_size=3GB
      -c work_mem=128MB
      -c maintenance_work_mem=256MB
      -c checkpoint_completion_target=0.9
      -c wal_buffers=16MB
      -c max_connections=100

Adjust based on available RAM: shared_buffers = 25% of total RAM, effective_cache_size = 75% of total RAM.

Concurrency settings

1
2
3
# In .env
IMMICH_JOBS_CONCURRENCY=5        # Increase for faster processing
MACHINE_LEARNING_WORKERS=2       # Parallel ML inference (RAM-intensive)

Initial import optimization

For large initial imports (100k+ photos):

  1. Increase job concurrency temporarily: IMMICH_JOBS_CONCURRENCY=10
  2. Disable thumbnails for offline assets until import completes
  3. Under Admin → Jobs → Thumbnail Generation → Run (after all assets are indexed)
  4. Then run: Smart Search, Face Detection, in that order
  5. Restore default concurrency after initial processing

Immich v1.130+ includes rewritten scan code that processes millions of assets in minutes rather than hours.


Monitoring

Enable Prometheus metrics:

1
2
3
4
  immich-server:
    environment:
      IMMICH_API_METRICS_ENABLED: "true"
      IMMICH_MICROSERVICES_METRICS_ENABLED: "true"

Metrics available at :8081/metrics (server) and :8082/metrics (microservices). Key metrics to watch:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Job queue depth (should trend toward 0)
immich_jobs_queue_waiting_total

# Jobs completed per minute
rate(immich_jobs_completed_total[5m])

# ML inference latency
immich_ml_request_duration_seconds

# Active database connections
pg_stat_activity_count{datname="immich"}

Keeping Immich Updated

Immich releases frequently (weekly minor releases). The easiest update workflow:

1
2
3
4
cd /opt/immich
docker compose pull
docker compose up -d
docker image prune -f   # Clean up old images

Or pin to a specific version in .env:

1
IMMICH_VERSION=v1.143.0

Check the release notes before each update — Immich occasionally has breaking changes that require database migrations (these run automatically on startup, but it’s worth knowing what changed).


What Immich Can’t Do Yet

Honest caveats for managing expectations:

  • Smart albums are manual — Immich groups faces automatically, but rule-based “show me all photos in Paris this year” smart albums aren’t fully implemented yet. Filtering and search accomplish the same goal, just without automatic album creation.
  • OCR text search is in development but not yet stable
  • Map view works, but offline tile servers require additional configuration
  • Sharing is functional but not as polished as Google Photos’ sharing UX

The project moves fast. Features that were missing six months ago often land in the next release.


Summary

Immich is the closest self-hosted equivalent to Google Photos. The architecture — NestJS server, background microservices, FastAPI ML service, pgvector for semantic search — is solid enough for production home use. With hardware acceleration, face recognition and CLIP search run fast enough that you barely notice they’re running on your own hardware.

The key decisions for a reliable deployment:

  1. Storage layout — Put the database and thumbnails on SSD. Originals can live on HDD.
  2. Hardware acceleration — Even an integrated Intel GPU dramatically speeds up ML processing.
  3. Mobile backup — Disable battery optimization for Immich on Android; on iOS, open the app regularly.
  4. Backups — Back up both the database dump and the asset files together; consistency matters.
  5. Face recognition strategy — Start with a high “minimum recognized faces” threshold, name the main clusters, then lower and iterate.

Your 30,000 photos can be home again.

Comments