Self-HostingDocker

Immich with Docker: your own Google Photos with face recognition and AI search (Linux, Windows, Mac)

Before you start

  • 14 min read
  • 18 to run in terminal
  • 7 file contents

Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#

Immich is a free, open-source photo and video server that works like Google Photos: your phone backs up to your own machine, and the server finds faces and understands search words like "dog on the beach".

Your photos stay on your disks. You get a web app, mobile apps, albums, sharing, people grouping and Smart Search. A small machine-learning container does the faces and the search. It runs on a CPU, and a GPU is optional.

WhatValue
Current versionImmich v3.3.5 (GitHub release of 21 October 2026, a security update)
Imagesghcr.io/immich-app/immich-server:v3.3.5 and ghcr.io/immich-app/immich-machine-learning:v3.3.5
Time neededAbout 20 minutes, plus the first image pull and the first photo scan
Ports2283 (web and API), bound to 127.0.0.1 in this guide
Official docsdocs.immich.app

Why v3.3.5 and not the release before it? We checked the issue tracker and discussions for this release. People report a few rough edges (an Android sync error after upload, a GPU that stays open while idle, a wrong storage number), but we found no repeated install failures and no data loss. The release also carries security fixes, so the maintainers ask everyone to update. We use the latest. Those reports are listed under Troubleshooting. Immich ships breaking changes often, so read the release notes before every update.

How it fits together#

Your phone talks to the server on port 2283. The server keeps metadata in Postgres, jobs in Valkey and the originals in the upload folder. For faces and search it asks the machine-learning container.

Diagram: a phone or browser reaches the immich-server container, optionally through Traefik over HTTPS; the server asks the machine-learning container, stores metadata in Postgres, queues jobs in Valkey and writes originals to the upload folder.

Before you start#

What you need, from the official requirements page:

LinuxWindowsMac
Best choiceDocker Engine with the Compose pluginDocker Desktop with WSL2, files kept inside WSLDocker Desktop
RAM6 GB minimum, 8 GB recommendedsame (give Docker Desktop enough)same
CPU2 cores minimum, 4 recommended; amd64 needs x86-64-v2sameIntel or Apple Silicon
DiskLocal SSD for the database; photos plus 10-20% for thumbnails and videosInside the WSL Linux filesystemAPFS is fine

Immich says Windows and Mac are supported but not the preferred hosts, and it gives less help for them. The database must never sit on a network share. On Windows it must not sit on C: through /mnt/c either, which is why the Windows steps below live inside WSL.

Check Docker first.

Run in terminalbash
docker --version
docker compose version
  • [ ] Docker Engine 25 or newer (older ones fail the health check, see Troubleshooting)
  • [ ] Port 2283 is free
  • [ ] You know where the backup copy will live (not the same disk)
  • [ ] For the Traefik section only: a domain, DNS and ports 80 and 443

Tested on: not run by DIY Lab Hub. The commands were checked against the official docs and release files of 26 October 2026.

Step 1: Create the folder and the .env file#

The data lives in ~/docker_volumes/immich. The .env file comes straight from the official v3.3.5 release; we only change four values and lock the file.

Linux#

Run in terminalbash
set -euo pipefail
umask 077
APP="$HOME/docker_volumes/immich"
TAG="v3.3.5"

mkdir -p "$APP"/{upload,postgres,model-cache}
cd "$APP"

curl -fL "https://github.com/immich-app/immich/releases/download/$TAG/example.env" -o .env

DB_SECRET="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')"
sed -i \
  -e 's|^UPLOAD_LOCATION=.*|UPLOAD_LOCATION=./upload|' \
  -e 's|^DB_DATA_LOCATION=.*|DB_DATA_LOCATION=./postgres|' \
  -e "s|^IMMICH_VERSION=.*|IMMICH_VERSION=$TAG|" \
  -e "s|^DB_PASSWORD=.*|DB_PASSWORD=$DB_SECRET|" \
  .env
unset DB_SECRET
chmod 600 .env

Windows (PowerShell)#

Immich needs the database on a Linux filesystem. Open your WSL distribution from PowerShell, with Docker Desktop's WSL integration switched on for it.

Run in terminalpowershell
wsl --list --verbose
wsl --exec bash -lc 'docker version && docker compose version'
wsl

At the Linux prompt, run the Linux block above. In WSL, $HOME is the Linux home folder, not your Windows profile. From then on, PowerShell can manage the stack like this:

Run in terminalpowershell
wsl --exec bash -lc 'cd "$HOME/docker_volumes/immich" && docker compose ps'

Mac (Terminal)#

Start Docker Desktop. Then the same commands as Linux, with one change: macOS sed needs -i ''.

Run in terminalbash
set -eu
umask 077
APP="$HOME/docker_volumes/immich"
TAG="v3.3.5"

mkdir -p "$APP/upload" "$APP/postgres" "$APP/model-cache"
cd "$APP"

curl -fL "https://github.com/immich-app/immich/releases/download/$TAG/example.env" -o .env

DB_SECRET="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')"
sed -i '' \
  -e 's|^UPLOAD_LOCATION=.*|UPLOAD_LOCATION=./upload|' \
  -e 's|^DB_DATA_LOCATION=.*|DB_DATA_LOCATION=./postgres|' \
  -e "s|^IMMICH_VERSION=.*|IMMICH_VERSION=$TAG|" \
  -e "s|^DB_PASSWORD=.*|DB_PASSWORD=$DB_SECRET|" \
  .env
unset DB_SECRET
chmod 600 .env

Check it worked:

Run in terminalbash
grep -E '^(UPLOAD_LOCATION|DB_DATA_LOCATION|IMMICH_VERSION)=' .env

Expected output:

File contenttext
UPLOAD_LOCATION=./upload
DB_DATA_LOCATION=./postgres
IMMICH_VERSION=v3.3.5

Relative paths start at the project folder, so everything stays under ~/docker_volumes/immich. The password must use only letters and digits, which the generated one does.

The Compose file#

This is the official v3.3.5 docker-compose.yml (it has no obsolete version: line). We made three small changes: the port is bound to 127.0.0.1, the model cache is a folder next to the rest, and restart: unless-stopped. The Postgres and Valkey image pins are the ones the release ships. Do not swap them for other tags. Save it as docker-compose.yml in the same folder (in WSL on Windows).

File contentyaml
name: immich

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    volumes:
      # Do not edit the next line. Change UPLOAD_LOCATION in .env instead.
      - ${UPLOAD_LOCATION}:/data
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    ports:
      - '127.0.0.1:2283:2283'
    depends_on:
      - redis
      - database
    restart: unless-stopped
    healthcheck:
      disable: false

  immich-machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    volumes:
      - ./model-cache:/cache
    env_file:
      - .env
    restart: unless-stopped
    healthcheck:
      disable: false

  redis:
    container_name: immich_redis
    image: docker.io/valkey/valkey:9@sha256:c123e3715db63d06d4ad6964884037aa0d5d4d703939b9929954112889708e1d
    healthcheck:
      test: redis-cli ping | grep -q PONG || exit 1
    restart: unless-stopped

  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'
      # Uncomment if your database is not on an SSD:
      # DB_STORAGE_TYPE: 'HDD'
    volumes:
      # Do not edit the next line. Change DB_DATA_LOCATION in .env instead.
      - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
    shm_size: 128mb
    restart: unless-stopped
    healthcheck:
      disable: false

If a newer release changes this file, take the new official one and re-apply your three changes. Never use the file from the main branch with a release.

Mac with Docker Desktop: if it complains about /etc/localtime, delete that one line and set TZ=Etc/UTC (or your zone) in .env.

Step 2: Start it#

Linux#

Run in terminalbash
cd ~/docker_volumes/immich
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps

Windows (PowerShell)#

Run in terminalpowershell
wsl --exec bash -lc 'cd "$HOME/docker_volumes/immich" && docker compose config --quiet && docker compose pull && docker compose up -d && docker compose ps'

Mac (Terminal)#

Same commands as Linux.

Check it worked: all four containers should show as running and, after a minute, healthy. The first start downloads the images and the Postgres setup, so be patient.

Run in terminalbash
docker compose logs --tail=50 immich-server

Do not paste a full docker compose config into a forum; it can print your database password.

First-run setup and phone backup#

  1. Open http://localhost:2283 on the server machine.
  2. Click Getting Started and create the admin account. The first account is the admin.
  3. Create a normal user for everyday use, and one per family member.
  4. Upload a few test photos, a video and some pictures with faces. Wait until the background jobs finish before you judge search and people.
  5. Install the Immich app on your phone from the links on the official site.
  6. Enter the server address, log in, open the cloud icon, choose albums and switch backup on.

Your phone's localhost is the phone, not the server. For a home trial, either change the port line to your server's LAN address (for example <server-lan-ip>:2283:2283), or use the HTTPS setup further down. Backup is Wi-Fi only by default. On iOS, keep Background App Refresh on.

Before you delete anything from the phone, check in the web app that your test photos arrived and open one at full size.

Existing photo folders (external library)#

An external library indexes a folder without moving it. Mount it read-only into the server container, in docker-compose.yml:

File contentyaml
    volumes:
      - ${UPLOAD_LOCATION}:/data
      - /etc/localtime:/etc/localtime:ro
      - /path/to/your-photos:/mnt/photos:ro

Then recreate and check the folder is visible inside the container:

Run in terminalbash
docker compose up -d
docker exec immich_server ls /mnt/photos

In the web app go to Administration, External Libraries, create a library, pick the owner, add /mnt/photos as the import path and scan. Use the path inside the container, not the host path. On Windows the folder must be reachable inside WSL, for example /mnt/d/Photos. Moving a file later makes Immich treat it as new. These originals are not in the upload folder, so back them up separately.

GPU for faces and search (optional)#

Start on the CPU and make sure everything works. Then, if you have a supported GPU, speed up the machine-learning container. Download the helper file for your version:

Run in terminalbash
cd ~/docker_volumes/immich
curl -fL https://github.com/immich-app/immich/releases/download/v3.3.5/hwaccel.ml.yml -o hwaccel.ml.yml

For NVIDIA, change the machine-learning service like this and keep the rest of it:

File contentyaml
  immich-machine-learning:
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}-cuda
    extends:
      file: hwaccel.ml.yml
      service: cuda
BackendImage suffixNeeds
NVIDIA CUDA-cudaCompute capability 5.2+, driver 545+; on Linux the NVIDIA Container Toolkit
Intel OpenVINO-openvino (openvino-wsl service on WSL)Intel GPU and device access
AMD ROCm-rocmSupported AMD GPU, 35 GiB free disk for the image

Apple GPUs are not supported through Docker, so Mac stays on the CPU. After docker compose up -d, the machine-learning log lists the available providers (look for CUDAExecutionProvider). The feature is marked experimental in the docs. 3.3.0 added an optional model setting, MACHINE_LEARNING_MODEL_REVISION=v2, which you can put in .env.

Put it behind HTTPS with Traefik#

This is for a server with a domain. If you only use Immich at home on localhost, skip it. It assumes Traefik already runs with an HTTPS entrypoint called websecure and a certificate resolver called letsencrypt; use your own names.

Run in terminalbash
docker network create proxy

Change the server service (and remove its ports: lines, so only Traefik can reach it):

File contentyaml
  immich-server:
    networks:
      - default
      - proxy
    labels:
      traefik.enable: "true"
      traefik.docker.network: "proxy"
      traefik.http.routers.immich.rule: "Host(`photos.<your-domain>`)"
      traefik.http.routers.immich.entrypoints: "websecure"
      traefik.http.routers.immich.tls: "true"
      traefik.http.routers.immich.tls.certresolver: "letsencrypt"
      traefik.http.services.immich.loadbalancer.server.port: "2283"

networks:
  proxy:
    external: true

Immich must live at the root of a hostname, not under /immich. Two more things from the official reverse-proxy page:

  • Upload time. Traefik cuts a request after 60 seconds by default, so big videos fail with error 499. In Traefik's static config raise the limits on your HTTPS entrypoint:
File contentyaml
entryPoints:
  websecure:
    address: ":443"
    transport:
      respondingTimeouts:
        readTimeout: 600s
        idleTimeout: 600s
  • Upload size. The official Traefik example sets no body-size limit, so nothing to copy from Nginx. If you added a buffering middleware or a tunnel in front, check its limit.

If you use the HTTP-01 certificate challenge, make sure /.well-known/immich still reaches Immich.

Security and testing#

  • Keep Immich off the open internet unless you must. Do not forward port 2283 on your router. For remote access from your phone, use a VPN or a private network first.
  • .env holds the database password: chmod 600 .env and never commit it.
  • Postgres and Valkey publish no ports. Keep it that way.
  • Use a strong admin password and give each person their own account.
  • Update for security fixes after a backup. This release is one of those.

Test it:

Run in terminalbash
curl -I http://127.0.0.1:2283
docker compose ps

With Traefik:

Run in terminalbash
curl -I https://photos.<your-domain>

A normal answer is an HTTP 200 for the page.

Troubleshooting#

ProblemSolution
unknown shorthand flag: 'd' in -dOld or wrong Docker setup. Install the Compose plugin and use docker compose, not docker-compose.
'name' does not match any of the regexes: '^x-'The old docker-compose program reads a modern file. Replace it with the Compose plugin.
can't set healthcheck.start_interval as feature require Docker Engine v25 or laterUpdate Docker Engine. The docs also allow commenting out start_interval.
Uploads stop after about a minute, error 499Raise Traefik readTimeout and idleTimeout to 600s (see above).
ENOSPC: System limit for number of file watchers reachedAn external library watcher hit the Linux inotify limit. Raise fs.inotify.max_user_watches or use scheduled scans.
Machine learning is slow or the GPU is not useddocker compose logs immich-machine-learning and look for the provider line. Fall back to the CPU image to isolate the problem.
Phone says server not reachablelocalhost is the phone. Use the LAN address or the HTTPS address.

open ...: permission denied#

The official docs link this message to a wrong Docker install. Check that your user can read .env, but do not make it world-readable.

relation already exists#

This comes from a command-line restore into a database that is not empty. Use the web restore flow, or restore into a brand-new empty target. Never delete the production database folder as a first fix.

Known bugs#

These were open in the issue tracker for 3.3.5 when we checked. We could not confirm a fix, so check the links.

  • Android sync error after a successful upload, SQLiteException(787): FOREIGN KEY constraint failed (#32685). The upload itself works. Check the asset in the web app and keep the originals.
  • The machine-learning container keeps an NVIDIA GPU open while idle (#32664). It matters if you share the GPU or care about power.
  • OpenVINO cast error (#32650). Try the CPU image to find out if the backend is the cause.
  • Wrong storage shown (discussion #32661). Cosmetic.

Useful commands:

Run in terminalbash
docker compose ps
docker compose logs --tail=100 immich-server
docker compose logs --tail=100 database

Updates and backups#

Immich is under heavy development and releases often break something small. Update on purpose, never blindly:

  1. Read the release notes for every version between yours and the target.
  2. Make a backup (below) and make sure a restore test worked once.
  3. Download the new release's docker-compose.yml to a separate name and compare it with yours.
  4. Change IMMICH_VERSION in .env, then pull and start:
Run in terminalbash
cd ~/docker_volumes/immich
docker compose pull
docker compose up -d
docker compose ps

Going back to an older tag does not undo database changes. Recovery means restoring the backup you took before the update. Two notes from the current docs: since v3 the amd64 machine-learning image needs an x86-64-v2 CPU, and backups made after the move to VectorChord need a database image that includes VectorChord (the pinned one does).

What to back up#

Immich's built-in database dump runs daily at 02:00, keeps 14 copies and writes them to upload/backups. It does not contain your photos. Back up all of these, and keep one copy on a different disk:

  • a database dump,
  • the whole upload/ folder,
  • external-library originals,
  • .env and docker-compose.yml.

Backup script (Linux, Mac, or inside WSL)#

The script stops the server for a moment so the dump and the files match. Replace the backup folder with a real path on another disk.

Run in terminalbash
set -euo pipefail
cd ~/docker_volumes/immich
DEST="/path/on/other-disk/immich/$(date +%Y%m%d-%H%M%S)"
umask 077
mkdir -p "$DEST"

docker compose stop immich-server
trap 'docker compose start immich-server >/dev/null' EXIT

docker exec immich_postgres pg_dump --clean --if-exists --dbname=immich --username=postgres | gzip > "$DEST/database.sql.gz"
gzip -t "$DEST/database.sql.gz"
tar -czf "$DEST/upload.tar.gz" -C ./upload .
cp .env docker-compose.yml "$DEST/"

Schedule it at 03:00 on Linux or Mac with crontab -e:

File contenttext
0 3 * * * /home/<you>/docker_volumes/immich/backup.sh

On Windows, create a Task Scheduler task that runs wsl --exec bash -lc "$HOME/docker_volumes/immich/backup.sh" every night.

Restore test#

Do this on a second machine or a VM, never on your live server.

  1. Install the same pinned version with a fresh, empty postgres folder.
  2. Unpack upload.tar.gz into the new upload folder.
  3. Start the stack and open the web app. On the welcome screen choose Restore from backup, not "Getting Started".
  4. Pick database.sql.gz and wait for it to finish.
  5. Log in, open a few full-size photos, play a video, look at People and run a Smart Search.

Write down the date of the successful test. A backup you never restored is only a hope.

Do not run docker compose down -v casually. If the database lives in a named volume, it removes it.

Performance and resources#

  • Plan for 4 cores and 8 GB RAM. The first import is heavy because Immich makes thumbnails, reads faces and builds the search index. That settles after a few hours or days, depending on the library.
  • Keep Postgres on a local SSD. This helps more than a GPU.
  • Plan disk space for thumbnails, transcoded video, the model cache and backups, not just the originals.
  • On Windows and Mac, Docker Desktop's VM has its own memory limit. Raise it in Docker Desktop settings.
  • A higher machine-learning concurrency uses more video memory, so raise it step by step.
  • Watch it live:
Run in terminalbash
docker stats

We did not measure throughput on any hardware, so we give no photos-per-second numbers.

FAQ#

Can Immich fully replace Google Photos? It covers the core: phone backup, albums, sharing, faces and search. You now look after storage, updates and backups yourself. Test your own phone and sharing needs before you delete anything.

Do I need a GPU? No. The CPU works. A supported GPU only makes faces and search faster.

Can I keep my folder structure? Yes. Add the folder as a read-only external library. Plan the layout first, because moving files later makes them new assets.

Does the automatic database backup include my photos? No. It holds metadata only. Back up the upload folder and any external folders too.

Should I let it update itself? We say no. Keep the version pinned, read the release notes, take a backup, then update. That avoids surprises and still lets you take security fixes quickly.

Sources#

Checked against the official docs on 2026-10-26 (the docs pages show an update date of 21 October 2026). Not run by DIY Lab Hub.