Skip to content

Immich Photo Backup

Overview

Immich is a self-hosted, high-performance photo and video backup solution that aims to directly replace Google Photos. Unlike traditional file managers, Immich relies heavily on machine learning to provide features like facial recognition, object detection (e.g., "search for 'cat'"), and map-based exploration.

The architecture is microservice-based, meaning it runs several distinct containers (Server, Machine Learning, Postgres, Valkey/Redis) that work in concert to handle massive upload queues and background processing tasks. Because the machine-learning container talks to the server over HTTP, it doesn't have to run on the same machine.


How I run it

Immich is split across two hosts:

Host Containers Why there
nas immich-server, Postgres (immich-app/postgres with VectorChord), Valkey The photo library lives on the NAS pool, so the server writes uploads straight to local disk. Postgres sits on the NAS's SSD, not the pool.
server2 immich-machine-learning (-openvino image) Face detection, CLIP and smart search are the CPU-heavy part. The NAS has an older i5-8500T and already carries Plex and Frigate; the apps box has a newer i5-12500T with a UHD 770 iGPU and plenty of headroom, so ML runs there with OpenVINO.

The server finds ML through IMMICH_MACHINE_LEARNING_URL. Uploads, thumbnails and video transcoding (Quick Sync on the NAS's own iGPU) stay on the NAS; only image previews travel to the ML host.

Server and ML must run the same version

Both images are pinned to the same tag and bumped together, by hand, in one commit. Renovate groups immich-server and immich-machine-learning into one PR that is never automerged. A mismatched pair causes odd bugs (failed jobs, wrong embeddings), see the remote ML guide.


Installation

Both stacks start from the official release files: docker-compose.yml, hwaccel.transcoding.yml (server side) and hwaccel.ml.yml (ML side). Keep the hwaccel file next to the compose file that extends it:

# on the NAS (server stack)
wget https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml
# on the ML host
wget https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml

Server, database and Valkey (NAS)

Hardware note

The NAS has an Intel i5-8500T and no discrete GPU, so transcoding uses service: quicksync (it just passes /dev/dri into the container). With an Nvidia card use nvenc instead.

compose.yaml (NAS)
name: immich
services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:v3.3.5
    extends:
      file: hwaccel.transcoding.yml
      service: quicksync # Intel iGPU; nvenc / vaapi / rkmpp for other hardware
    volumes:
      - ${UPLOAD_LOCATION}:/data
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    environment:
      # ML runs on another host; must be the same Immich version as this server
      - IMMICH_MACHINE_LEARNING_URL=http://<ML_HOST_IP>:3003
    ports:
      - '2283:2283'
    depends_on:
      - redis
      - database
    restart: always
    healthcheck:
      disable: false

  redis:
    container_name: immich_redis
    image: docker.io/valkey/valkey:9@sha256:418652cfb58ef879d4978c33553735d7147016032d5aefaa14c828e611eb9dfd
    healthcheck:
      test: redis-cli ping || exit 1
    restart: always

  database:
    container_name: immich_postgres
    image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_DB: ${DB_DATABASE_NAME}
      POSTGRES_INITDB_ARGS: '--data-checksums'
      # DB_STORAGE_TYPE: 'HDD'   # only if the database is NOT on an SSD
    volumes:
      - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
    shm_size: 128mb
    restart: always
.env.example
# Photo library on the pool, Postgres on SSD
UPLOAD_LOCATION=/mnt/storage/photos/immich/library
DB_DATA_LOCATION=/mnt/appdata/immich/postgres
TZ=<TZ>   # e.g. Etc/UTC
DB_PASSWORD=<YOUR_PASSWORD>   # A-Za-z0-9 only
DB_USERNAME=postgres
DB_DATABASE_NAME=immich

The tags are literal in the compose file rather than ${IMMICH_VERSION}: Renovate can only bump what it can see, and a version that lives in a host-only .env drifts silently (that's exactly how my ML container once ended up on a different version than the server).

Machine learning (separate host)

compose.yaml (ML host)
name: immich-ml
services:
  immich-machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:v3.3.5-openvino
    extends:
      file: hwaccel.ml.yml
      service: openvino # Intel iGPU; cuda / rocm / armnn / rknn for other hardware
    volumes:
      - /mnt/appdata/immich/machinelearning/model-cache:/cache
    ports:
      - '3003:3003'
    restart: always
    healthcheck:
      disable: false

No .env is needed on this side. The model cache is a bind mount on the SSD, so models survive a container recreate and don't re-download.

The ML container has no authentication

Per Immich's own docs it has "no security measures whatsoever". Only the Immich server should be able to reach port 3003: firewall it to the NAS's address, or bind it to an interface only the server can reach. Check that it answers from the server: curl http://<ML_HOST_IP>:3003/ping should return pong.

IMMICH_MACHINE_LEARNING_URL sets the default URL; you can also add URLs under Administration > Settings > Machine Learning. If you list several, Immich tries them in order, so you could keep a local ML container as a fallback. I don't: with only the remote URL, Smart Search and Face Detection jobs fail while the ML host is down and need a re-run ("Missing" on the Job Status page) afterwards.


Initial Setup

Start the ML stack first, then the server stack (docker compose up -d in each directory).

Navigate to http://<YOUR_SERVER_IP>:2283 to see the "Getting Started" wizard.

  1. Click Getting Started.
  2. Create Admin Account: Enter your email, name, and password.
  3. Log in: You will be taken to the main timeline.

Hardware Acceleration

Video Transcoding (Intel QuickSync)

Why use it: When you view a 4K video on a phone over a slow connection, Immich must transcode (shrink) that video in real-time. Doing this on the CPU will pin your processor to 100% usage and create stuttering. QuickSync offloads this to the iGPU.

How to configure: The hwaccel.transcoding.yml in the compose file handles the backend permissions, but you must enable it in the UI.

  1. Go to Administration (top right) -> Settings -> Video Transcoding.
  2. Find Hardware Acceleration.
  3. Select Quick Sync from the dropdown.
  4. Click Save.

Machine Learning (OpenVINO)

Why use it: Immich scans every photo for faces and objects. On a raw CPU, scanning 1,000 photos can take hours. OpenVINO optimizes these mathematical operations for Intel CPUs, significantly increasing indexing speed.

How to configure: There is no UI toggle for this. It is controlled entirely by the image tag (-openvino) and the extends block in the compose file.

Verification: Run the following on the ML host to check if it loaded correctly:

docker logs immich_machine_learning 2>&1 | grep -i openvino
  • Success: You will see a line stating Loaded execution provider: OpenVINO.
  • Failure: If you only see CPUExecutionProvider, check that your image tag ends in -openvino.

Database & Storage

PostgreSQL with pgvector

Immich requires a specific version of Postgres equipped with pgvector. This extension allows the database to store "vector embeddings"—mathematical representations of images. This enables smart searches (e.g., searching for "red truck" finds trucks without manual tags).

Performance Tip

Unlike Nextcloud, you rarely touch the DB config manually. However, ensure DB_DATA_LOCATION in your .env points to fast storage (SSD/NVMe). Vector searches on HDDs are noticeably slower.

Storage Template

By default, Immich uploads files into a generic folder structure. If you ever want to access your files directly on the disk (outside of Immich), this is difficult to navigate.

The Fix: Configure this before uploading your library.

  1. Go to Administration -> Settings -> Storage Template.
  2. Enabled: Toggle ON.
  3. Template: Select or customize your structure.
  4. Recommendation: {{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}
  5. Result: /2024/2024-12-25/IMG_1234.jpg

  6. Click Save.


Post-Install Monitoring

GPU Utilization

Since you are using hardware acceleration, verify the load is hitting the GPU and not the CPU.

The Tool: intel_gpu_top

  1. Install on Host (Debian; run it on whichever host you're checking: NAS for transcoding, ML host for OpenVINO):

    sudo apt install intel-gpu-tools
    

  2. Run Monitor:

    sudo intel_gpu_top
    

  3. Test: Open Immich and scroll rapidly through the timeline (triggers thumbnail generation) or play a large video. You should see the Video or Render bars spike.


Sources & further reading