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.
Try it locally
Section titled “Try it locally”On your own machine, with no proxy, domain or certificate. From apps/immich/:
cp .env.local.example .env.local
# The secrets ship empty; generate the ones this stack needsecho "DB_PASSWORD=$(openssl rand -hex 32)" >> .env.local
mkdir -p volumes/local/redissudo chown 999:999 volumes/local/redis
docker compose -f docker-compose.local.yml --env-file .env.local up -dThen open http://localhost:2283. Stop it again with the same command and down.
Installation
Section titled “Installation”From apps/immich/:
-
Copy the environment file and set
APP_TRAEFIK_HOST,TZandUPLOAD_LOCATION— the last one is where photos are stored and may point outside the stack directory:Terminal window cp .env.example .env -
Generate the database password. Immich accepts only letters and digits, so the base64 output is stripped of its punctuation:
Terminal window mkdir -p .secretsopenssl rand -base64 32 | tr -d '/+=\n' > .secrets/db_pwd.txtsed -i "s|^DB_PWD_INLINE=.*|DB_PWD_INLINE=$(cat .secrets/db_pwd.txt)|" .envThe value appears twice: as the secret file for PostgreSQL and inline for Immich, which has no
_FILEform. -
Create the volumes:
Terminal window mkdir -p volumes/postgres volumes/redis volumes/model-cache volumes/library -
Start it. First-run migrations take around two minutes:
Terminal window docker compose up -ddocker compose logs immich-app --followWait for
Immich Server is listening on. -
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 — setAPP_TRAEFIK_ACCESS=access-publicin.envand rundocker compose up -dif the phones are to upload from outside the VPN.
Verify
Section titled “Verify”docker compose ps # four services, healthycurl -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.
Going further
Section titled “Going further”Importing an existing library
Section titled “Importing an existing library”The official CLI imports a folder. Create an API key under Account Settings → API Keys first:
npm install -g @immich/cliimmich 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:
./immich-go upload from-google-photos \ --server https://<APP_TRAEFIK_HOST> --api-key <API-KEY> /path/to/takeoutStorage and acceleration
Section titled “Storage and acceleration”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.
Backup
Section titled “Backup”| 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 psqlUPLOAD_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.
Restore
Section titled “Restore”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:
docker compose stop immich-appdocker compose exec -T db sh -c 'pg_dump --username="$POSTGRES_USER" "$POSTGRES_DB"' | gzip > dump.sql.gzdocker compose start immich-appRestore 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:
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.
Updates
Section titled “Updates”-
Read the release notes. Immich changes quickly and migrations are frequent.
-
Back up the database and the library together.
-
Raise
APP_TAGin.env, then:Terminal window docker compose pull && docker compose up -ddocker compose logs immich-app --follow -
Open the library and run a search. A migration that touched the embeddings surfaces there rather than at start.
Troubleshooting
Section titled “Troubleshooting”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.
Repository files
Section titled “Repository files”apps/immich/README.md— setup, security model, the full restore procedureapps/immich/UPSTREAM.md— source and upgrade checklist
Official sources
Section titled “Official sources”© 2026 SecDockBlueCode Apache-2.0Site content CC BY-NC 4.0