Ditching Google Photos: Tuning Immich on Docker with Vector Search & Facial Recognition

Written by

in

, ,

Cloud photo storage is one of the sneakiest vendor lock-ins in modern tech. You start with a free 15GB tier, and before you know it, you are paying a monthly subscription to Google One or Apple iCloud because your library of 4K family videos and lab project photos has exceeded 2 Terabytes. Beyond the recurring cost, the idea of a hyperscaler scanning your personal photo library for ad targeting and model training never sits right. When our family photo archive crossed 65,000 assets, I migrated everything to self-hosted Immich on Docker.

Immich has become the definitive self-hosted photo platform. It isn’t just a static web gallery like Nextcloud Photos or older PHP albumsβ€”it replicates the seamless, lightning-fast experience of Google Photos. You get automatic mobile background sync, facial clustering, interactive map views, and natural language semantic search. Here is how I deployed Immich on docker02, tuned its machine learning pipeline, and laid out the storage architecture for peak performance.

1. The Architecture: Microservices Behind the Lens

Immich isn’t a single monolithic binary. It is engineered as a distributed microservices stack designed to offload heavy computational workloads (like face recognition and video transcoding) from the web server:

  1. immich-server: The core API engine (Node.js/NestJS) that handles asset uploads, user authentication, thumbnail serving, and library metadata.
  2. immich-machine-learning: A Python microservice running ONNX runtime models. It handles facial recognition via InsightFace and natural language search embeddings via CLIP.
  3. immich-postgres: A specialized PostgreSQL 14 instance compiled with pgvectors and vectorchord. This allows the database to store high-dimensional machine learning vector embeddings directly alongside traditional SQL metadata and perform sub-second nearest-neighbor search.
  4. immich-redis (Valkey 9): A dedicated Valkey instance acting as an in-memory queue manager for background jobs (thumbnail generation, video encoding, face detection, EXIF extraction).

2. Production Docker Compose Configuration

Here is our battle-tested compose configuration running on docker02 (IP 10.1.20.19, port 2283):

# /opt/stacks/immich/docker-compose.yml excerpt
name: immich

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:
    volumes:
      - :/data
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    ports:
      - '2283:2283'
    depends_on:
      - redis
      - database
    restart: always

  immich-machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:
    volumes:
      - model-cache:/cache
    environment:
      # Prevent PyTorch/ONNX from saturating all host CPU cores
      - OMP_NUM_THREADS=2
      - RAY_ACCELERATOR=cpu
    deploy:
      resources:
        limits:
          cpus: '3.0'
          memory: 4096M
    restart: always

  redis:
    container_name: immich_redis
    image: docker.io/valkey/valkey:9
    restart: always

  database:
    container_name: immich_postgres
    image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
    environment:
      POSTGRES_PASSWORD: 
      POSTGRES_USER: 
      POSTGRES_DB: 
      POSTGRES_INITDB_ARGS: '--data-checksums'
    volumes:
      - :/var/lib/postgresql/data
    shm_size: 256mb
    restart: always

volumes:
  model-cache:

3. Conquering the ML Core Starvation Problem

The Big Gotcha: When you bulk-import 65,000 photos, the machine learning container will enthusiastically spawn threads for every logical CPU core on the host. In our lab, this pegged the CPU at 100%, causing Zabbix alert storms and making other containers sluggish.

To prevent machine learning tasks from choking the Docker host, two critical settings were applied:

  • Thread Concurrency Throttling: We set OMP_NUM_THREADS=2 in the environment variables. This forces the ONNX Runtime / OpenMP thread pool to restrict facial recognition inference to two worker threads per task instead of flooding the entire vCPU pool.
  • Docker Resource Capping: We placed a hard cap of 3.0 CPUs and 4GB RAM on the immich-machine-learning container. If an intensive batch of 4K video frames enters the facial detection queue, it runs quietly in the background without starving the web server or database.

4. Storage Tiering: NVMe vs. Mass Storage

A common mistake in self-hosting photo libraries is putting the database on spinning rust or over an NFS network mount. Immich generates massive amounts of small files: square thumbnails, micro-previews, blurhash strings, and high-dimensional vector embeddings.

We separated storage into two distinct performance tiers:

  • Fast IOPS Tier (Local NVMe/SSD): The PostgreSQL database (/var/lib/postgresql/data) and Valkey in-memory cache live on fast local SSD storage on docker02. This ensures that querying timeline grids with 50,000 items renders in under 50 milliseconds.
  • Bulk Storage Tier (High-Capacity SAN/NAS): The original RAW files, JPEG originals, and transcoded MP4 video proxies (UPLOAD_LOCATION) map to our redundant high-capacity storage pools.

5. The Magic of Semantic Vector Search

The feature that completely won over my family is Immich’s natural language semantic search. Because every photo is processed through a local CLIP (Contrastive Language-Image Pre-Training) model, you can search your library using concepts rather than tags:

  • “Dog playing in the snow” β€” Instantly brings up winter photos from five years ago.
  • “Receipts and invoices” β€” Surfaces photos of hardware receipts and shipping labels.
  • “Server rack with blue Ethernet cables” β€” Pulls up historic wiring photos from my homelab setup.

All of this happens locally inside the Docker container. Not a single byte of biometric facial data or photo content ever touches an external third-party server.

6. Seamless Mobile Experience

The Immich iOS and Android apps work flawlessly. Whenever our phones connect to our home Wi-Fi (NET-WORKSTATIONS-VLAN10), new photos immediately sync to the server in the background. When we are on the road, our phones connect through our persistent WireGuard VPN tunnel back to our UniFi Dream Machine, allowing instant photo backups from hotel Wi-Fi or cellular.

We’ve cut out the monthly cloud storage tax, reclaimed complete privacy over our family memories, and built a photo management platform that is faster than the commercial clouds.