Deploy Immich on Docker: Self-Hosted Google Photos Alternative with AI

HomeLab tutorial - IT technology blog
HomeLab tutorial - IT technology blog

Approach Comparison: Finding a True Cloud Photo Alternative

At 2:15 AM on a Sunday, my home server alerts went wild. My family had just dumped 65 GB of 4K vacation footage and RAW files into Nextcloud. The server buckled immediately. Preview generation spun up 16 PHP-FPM workers, Redis blew past its 2 GB memory cap, and database lock contention froze the web UI for over an hour. That crash drove home a hard reality. General-purpose file sync platforms simply fall apart under heavy media workloads.

If you want to replace Google Photos or iCloud with a homelab setup, you generally choose between three design approaches:

  • Generalist Storage Plugins (Nextcloud Memories, FileRun): These run on top of conventional directory trees. They handle standard JPEG galleries reliably. However, background indexing, file locks, and facial clustering quickly swamp multi-core CPUs during concurrent 20 GB phone uploads.
  • Archival Indexers (PhotoPrism, LibrePhotos): Built primarily to scan static NFS mounts and existing folder structures. While their metadata tagging and EXIF parsing are rock solid, their background mobile sync remains clunky. Two family members backing up simultaneously often triggers sync stalls.
  • Dedicated Microservice Engines (Immich): Built from day one to match Google Photos. Immich breaks heavy tasks into isolated workers. Video transcoding, facial clustering, and CLIP vector search all run in dedicated asynchronous queues backed by Redis and Python ML containers.

Pros and Cons in Production

Immich delivers commercial-grade polish, but self-hosting it requires real architectural planning before you migrate your household.

The Advantages

  • On-Device Semantic & Face Search: PostgreSQL stores facial embeddings and ViT-B-32 CLIP vectors via pgvector. Search for “kids eating ice cream at the beach” or “red Honda Civic in the snow.” Immich surfaces the exact shots in under 80 milliseconds across 90,000 pictures.
  • Native Mobile Experience: The Flutter-based mobile apps for iOS and Android support background camera uploads, multiple user accounts, shared partner libraries, and instant timeline scrubbing that feels indistinguishable from commercial cloud apps.
  • Isolated Resource Allocation: The Python machine learning service runs independently. You can bind inference to an Intel iGPU via QuickSync or an Nvidia RTX card, set hard RAM limits, and keep background AI jobs from starving your host OS.

The Trade-offs and Maintenance Burden

  • Fast-Moving Release Cycle: Immich evolves quickly. Database migrations land frequently, which means you cannot leave it on unattended auto-updates without risking schema corruption.
  • Baseline Memory Footprint: A running stack—PostgreSQL, Redis, the core API server, and the ML engine—idles around 1.8 GB of RAM. During an initial import of 100,000 photos, usage easily hits 5 GB to 7 GB.

Hardware Sizing and Storage Architecture

Never mix transient application data with your primary media storage. Keeping high-IOPS database operations separate from write-heavy media volumes ensures the UI stays snappy even while four phones dump gigabytes of media in parallel.

Here is a battle-tested storage split for a multi-user household:

  • Compute Core: 4 CPU cores (x86_64) and 8 GB RAM minimum. An 8th-generation Intel Core i3-8100 or newer gives you an integrated UHD 630 GPU with QuickSync. That hardware engine transcodes 4K 60fps HEVC phone video down to 1080p VP9 streams in real time with barely 5% CPU usage.
  • Database Layer (Fast NVMe SSD): Place PostgreSQL database tables, Redis snapshots, and ML model caches on an NVMe drive. This guarantees low sub-millisecond seek times for search indexing and timeline pagination.
  • Bulk Storage Layer (ZFS or Btrfs Array): Mount an HDD pool to /mnt/storage/immich/upload. Phone camera uploads, Sony ARW files, and Apple ProRes clips stream directly to mechanical drives in sequential writes, leaving your database NVMe completely untroubled.

Implementation Guide: Deploying Immich on Docker

The following deployment uses Docker Compose, pgvector hardware acceleration, and clear storage volume boundaries.

Step 1: Directory Setup and Environment Variables

Create dedicated directories on the host and assign appropriate user ownership:

sudo mkdir -p /opt/immich/data/pgdata
sudo mkdir -p /opt/immich/data/model-cache
sudo mkdir -p /mnt/storage/immich/upload
sudo chown -R $USER:$USER /opt/immich
cd /opt/immich

Generate a .env file containing your volume mappings and database secrets:

cat << 'EOF' > .env
# Bulk storage for original photos and videos
UPLOAD_LOCATION=/mnt/storage/immich/upload

# Fast SSD storage for database and ML caches
DB_DATA_LOCATION=/opt/immich/data/pgdata
MODEL_CACHE_LOCATION=/opt/immich/data/model-cache

# Immich Version Tag
IMMICH_VERSION=release

# Database Credentials and Host
DB_HOSTNAME=database
DB_DATABASE_NAME=immich
DB_USERNAME=postgres
DB_PASSWORD=SecureProductionSecretPassphrase987!
EOF

Step 2: Construct the Docker Compose Stack

Create your docker-compose.yml file. This definition routes vector operations through pgvecto-rs and provisions a dedicated ML container with persistent model caching.

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:2283"
    depends_on:
      - redis
      - database
    restart: always

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

  redis:
    container_name: immich_redis
    image: docker.io/redis:6.2-alpine@sha256:eaba718fecd119618655c7854aeaa3db8037b467144e49970c94625b9045a43e
    healthcheck:
      test: redis-cli ping || exit 1
    restart: always

  database:
    container_name: immich_postgres
    image: tensorchord/pgvecto-rs:pg14-v0.2.1@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f1
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_DB: ${DB_DATABASE_NAME}
      POSTGRES_INITDB_ARGS: '--data-checksums'
    volumes:
      - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
    healthcheck:
      test: pg_isready --dbname="${DB_DATABASE_NAME}" --username="${DB_USERNAME}" || exit 1;
      interval: 10s
      timeout: 5s
      retries: 5
    restart: always

Step 3: Launch and Verify Service Health

Bring the containers up in detached mode:

docker compose up -d

Check the startup sequence across the microservices:

docker compose ps
docker compose logs -f immich-server

Watch for log confirmation that database migrations finished successfully and the HTTP listener is active on port 2283.

Step 4: Initial Admin Setup and Mobile Pairing

  1. Open your browser to http://<YOUR_SERVER_IP>:2283.
  2. Fill out the initial registration screen to generate your primary administrator credentials.
  3. Open Administration -> Settings -> Machine Learning. Adjust concurrent worker threads so they match your physical core count. This step prevents worker threads from saturating your system during bulk library imports.
  4. Install the Immich app on iOS or Android. Enter your server URL http://<YOUR_SERVER_IP>:2283, log in with your account, and enable automated background camera roll backups.

Step 5: Automated Database Backups

Containers are disposable, but your metadata and face clusters are not. Run a cron job every night to dump the database. Note that we omit the -t flag from docker exec so cron executes cleanly without requiring a pseudo-TTY:

cat << 'EOF' > /opt/immich/backup-db.sh
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/mnt/storage/backups/immich"
mkdir -p "${BACKUP_DIR}"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")

# Dump database without tty allocation for cron compatibility
docker exec immich_postgres pg_dumpall -c -U postgres | gzip > "${BACKUP_DIR}/immich_db_${TIMESTAMP}.sql.gz"
find "${BACKUP_DIR}" -type f -name "*.sql.gz" -mtime +14 -delete
EOF
chmod +x /opt/immich/backup-db.sh

Add this script to your host crontab with crontab -e to run at 3:00 AM daily:

0 3 * * * /opt/immich/backup-db.sh >> /var/log/immich-backup.log 2>&1

With this pipeline active, your photos remain self-hosted, fully searchable by AI, and entirely under your control.

Share: