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.
| What | Value |
|---|---|
| Current version | Immich v3.3.5 (GitHub release of 21 October 2026, a security update) |
| Images | ghcr.io/immich-app/immich-server:v3.3.5 and ghcr.io/immich-app/immich-machine-learning:v3.3.5 |
| Time needed | About 20 minutes, plus the first image pull and the first photo scan |
| Ports | 2283 (web and API), bound to 127.0.0.1 in this guide |
| Official docs | docs.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.
Before you start#
What you need, from the official requirements page:
| Linux | Windows | Mac | |
|---|---|---|---|
| Best choice | Docker Engine with the Compose plugin | Docker Desktop with WSL2, files kept inside WSL | Docker Desktop |
| RAM | 6 GB minimum, 8 GB recommended | same (give Docker Desktop enough) | same |
| CPU | 2 cores minimum, 4 recommended; amd64 needs x86-64-v2 | same | Intel or Apple Silicon |
| Disk | Local SSD for the database; photos plus 10-20% for thumbnails and videos | Inside the WSL Linux filesystem | APFS 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.
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#
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.
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:
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 ''.
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:
grep -E '^(UPLOAD_LOCATION|DB_DATA_LOCATION|IMMICH_VERSION)=' .env
Expected output:
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).
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#
cd ~/docker_volumes/immich docker compose config --quiet docker compose pull docker compose up -d docker compose ps
Windows (PowerShell)#
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.
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#
- Open
http://localhost:2283on the server machine. - Click Getting Started and create the admin account. The first account is the admin.
- Create a normal user for everyday use, and one per family member.
- Upload a few test photos, a video and some pictures with faces. Wait until the background jobs finish before you judge search and people.
- Install the Immich app on your phone from the links on the official site.
- 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:
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:
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:
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:
immich-machine-learning:
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}-cuda
extends:
file: hwaccel.ml.yml
service: cuda
| Backend | Image suffix | Needs |
|---|---|---|
| NVIDIA CUDA | -cuda | Compute 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 | -rocm | Supported 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.
docker network create proxy
Change the server service (and remove its ports: lines, so only Traefik can reach it):
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:
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.
.envholds the database password:chmod 600 .envand 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:
curl -I http://127.0.0.1:2283 docker compose ps
With Traefik:
curl -I https://photos.<your-domain>
A normal answer is an HTTP 200 for the page.
Troubleshooting#
| Problem | Solution |
|---|---|
unknown shorthand flag: 'd' in -d | Old 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 later | Update Docker Engine. The docs also allow commenting out start_interval. |
| Uploads stop after about a minute, error 499 | Raise Traefik readTimeout and idleTimeout to 600s (see above). |
ENOSPC: System limit for number of file watchers reached | An 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 used | docker 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 reachable | localhost 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:
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:
- Read the release notes for every version between yours and the target.
- Make a backup (below) and make sure a restore test worked once.
- Download the new release's
docker-compose.ymlto a separate name and compare it with yours. - Change
IMMICH_VERSIONin.env, then pull and start:
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,
.envanddocker-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.
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:
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.
- Install the same pinned version with a fresh, empty
postgresfolder. - Unpack
upload.tar.gzinto the newuploadfolder. - Start the stack and open the web app. On the welcome screen choose Restore from backup, not "Getting Started".
- Pick
database.sql.gzand wait for it to finish. - 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:
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.