Skip to content

Immich

Immich backs up photos and videos from phones and searches them by what is in them. Four containers — the server, a machine-learning worker, PostgreSQL with vector extensions, and Valkey for the job queue.

Last run on a host 8 Nov 2026 · data restored from a backup

Pinned at v3.3.6 (released 4 Nov 2026) · the newest upstream release

In the upstream images: 16 CRITICAL · 102 HIGH with a published fix — per image

To try it: Docker, and storage for whatever library you import. See Try it locally.

To deploy it behind the proxy: Traefik running, a domain, and storage for the library.

On your own machine, with no proxy, domain or certificate. From apps/immich/:

Terminal window
cp .env.local.example .env.local
# The secrets ship empty; generate the ones this stack needs
echo "DB_PASSWORD=$(openssl rand -hex 32)" >> .env.local
mkdir -p volumes/local/redis
sudo chown 999:999 volumes/local/redis
docker compose -f docker-compose.local.yml --env-file .env.local up -d

Then open http://localhost:2283. Stop it again with the same command and down.

From apps/immich/:

  1. Copy the environment file and set APP_TRAEFIK_HOST, TZ and UPLOAD_LOCATION — the last one is where photos are stored and may point outside the stack directory:

    Terminal window
    cp .env.example .env
  2. Generate the database password. Immich accepts only letters and digits, so the base64 output is stripped of its punctuation:

    Terminal window
    mkdir -p .secrets
    openssl rand -base64 32 | tr -d '/+=\n' > .secrets/db_pwd.txt
    sed -i "s|^DB_PWD_INLINE=.*|DB_PWD_INLINE=$(cat .secrets/db_pwd.txt)|" .env

    The value appears twice: as the secret file for PostgreSQL and inline for Immich, which has no _FILE form.

  3. Create the volumes:

    Terminal window
    mkdir -p volumes/postgres volumes/redis volumes/model-cache volumes/library
  4. Start it. First-run migrations take around two minutes:

    Terminal window
    docker compose up -d
    docker compose logs immich-app --follow

    Wait for Immich Server is listening on.

  5. Over the VPN, open https://<APP_TRAEFIK_HOST> and register. The first account to sign up becomes the administrator, with no setup secret; the route ships closed (access-vpn) for that reason. Once the administrator exists, a second sign-up is refused — set APP_TRAEFIK_ACCESS=access-public in .env and run docker compose up -d if the phones are to upload from outside the VPN.

Terminal window
docker compose ps # four services, healthy
curl -fsS https://<APP_TRAEFIK_HOST>/api/server/ping # {"res":"pong"}

Then upload one photo from the mobile app and confirm it appears with a thumbnail, and that search finds it by content. Those three together cover the server, the library path and the machine-learning worker; the ping covers only the first.

The official CLI imports a folder. Create an API key under Account Settings → API Keys first:

Terminal window
npm install -g @immich/cli
immich upload --recursive /path/to/photos \
--key <API-KEY> --server https://<APP_TRAEFIK_HOST>

For a Google Photos Takeout or a large library with duplicates, the community importer immich-go reads the Takeout’s sidecar metadata, skips files already in Immich by content hash, builds albums from folders and has a dry-run mode:

Terminal window
./immich-go upload from-google-photos \
--server https://<APP_TRAEFIK_HOST> --api-key <API-KEY> /path/to/takeout

UPLOAD_LOCATION can be an NFS or SMB share; volumes/postgres must stay on local storage, because PostgreSQL corrupts on network filesystems. Transcoding and machine-learning inference can use a GPU or NPU — upstream’s hardware acceleration guide has the extends: block and the image tag to use. The first inference downloads the CLIP model, several minutes on a slow link.

Database PostgreSQL in Immich’s own image, container immich-db, database immich, user immich
Password .secrets/db_pwd.txt
The library UPLOAD_LOCATION — the photos and videos, which may sit outside the stack directory
Rebuildable volumes/redis · volumes/model-cache
Quiescing Not needed for the dump. Pause uploads while copying a large library if the two halves must match exactly.

For borgmatic:

postgresql_databases:
- name: immich
username: immich
# The server's own client, inside its container
pg_dump_command: docker exec immich-db pg_dump
pg_restore_command: docker exec -i immich-db pg_restore
psql_command: docker exec -i immich-db psql

UPLOAD_LOCATION points outside the stack directory, which is why it gets forgotten. It is also the only irreplaceable part: albums, faces and metadata can all be rebuilt from the originals, and the originals cannot be rebuilt from anything.

A plain psql < dump.sql does not restore this database, and it does not report that it failed. The restore expects one database in plain SQL, taken with the server stopped so it cannot capture a torn state:

Terminal window
docker compose stop immich-app
docker compose exec -T db sh -c 'pg_dump --username="$POSTGRES_USER" "$POSTGRES_DB"' | gzip > dump.sql.gz
docker compose start immich-app

Restore it only into a fresh install — a database the Immich server has never started against. If the server has run, recreate volumes/postgres first; where the database cannot be started on its own, set DB_SKIP_MIGRATIONS=true before bringing the stack up, so the server does not migrate a database that is about to be replaced. Then load the dump exactly as below. Each part of the pipeline is required:

Terminal window
gunzip --stdout dump.sql.gz \
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
| docker compose exec -T db sh -c \
'psql --dbname="$POSTGRES_DB" --username="$POSTGRES_USER" \
--single-transaction --set ON_ERROR_STOP=on'
Part What it does
the sed rewrites search_path so the vector extension’s types and functions resolve — without it the objects depending on them are never created
--single-transaction a failure rolls the whole restore back rather than leaving a half-populated database that starts
ON_ERROR_STOP=on stops at the first error instead of continuing past it

Restore into the same database image version that produced the dump, not a stock postgres image.

Restore order: database, then the library, then the application.

  1. Read the release notes. Immich changes quickly and migrations are frequent.

  2. Back up the database and the library together.

  3. Raise APP_TAG in .env, then:

    Terminal window
    docker compose pull && docker compose up -d
    docker compose logs immich-app --follow
  4. Open the library and run a search. A migration that touched the embeddings surfaces there rather than at start.

Someone else is the administrator. The first account to register claimed it. There is no recovery other than rebuilding the instance.

The database refuses the password. Immich accepts only letters and digits. A password containing +, / or = is rejected — regenerate it as in step 2.

A restored instance starts but albums and search are empty. The dump was loaded without the search_path rewrite, so the objects depending on the vector extension were never created. The load reports no error. Restore again as shown under Restore.

Search returns nothing while photos are present. The machine-learning worker has not finished, or the model cache did not download. Check docker compose logs machine-learning.