Apps

Immich Docker Compose: Verified v3.2.4 File and RAM

Immich Docker Compose file verified on v3.2.4: 1.3 GiB idle, 3.3 GiB peak across four containers, plus the update, backup and restore steps we ran.

Edited by Simon Carter ·

Evidence: TestedBacked by a Docker run in our lab.How we label evidence

Tested: Immich v3.2.4 on · idle 1.3 GiB · peak 3.3 GiB

The Immich Docker Compose stack is four containers: the server, machine learning, Valkey (the cache, named redis in the file) and Postgres. We verified the file below on Immich v3.2.4 on Oct 1, 2026 and re-ran it on Oct 2, 2026 with the same image digests. Its health check passed after 13.1 s. It idled at 1.3 GiB once its memory had settled, used up to 2.2 GiB in its first two minutes, and peaked at 3.3 GiB while processing 20 uploaded photos on a 4-vCPU lab host. Plan for 8 GB of RAM. It differs from Immich’s own release file in two ways: named volumes instead of bind mounts, and exact image tags instead of ${IMMICH_VERSION}. Below are the problems we hit, a safe update, and a database backup and restore we ran.

Test box: Immich v3.2.4

Measured in our lab on . The numbers below come from the raw result file.

Idle RAM
1.3 GiB all containers, mean over 30 s after the stack settled (176 s after healthy)
Startup peak RAM
2.2 GiB highest total between healthy and settled
Peak RAM
3.3 GiB under load
Settled after
176 s after the first healthy response
Idle CPU
3.0% of one core, lab host
Startup
13 s to first healthy response
App version
v3.2.4
Tested
(compose file verified the same day)
Lab host
Intel(R) Xeon(R) Processor @ 2.10GHz, 4 cores, 15.7 GiB RAM, x86_64.Ubuntu 24.04.4 LTS, Docker 29.6.2.
How idle was measured
After the stack was healthy and set up, we sampled total RAM until it stayed within max(2% of its mean, 10 MiB) over 60 s (at least 120 s, at most 600 s), then measured idle over the next 30 s. Rule in full
Peak RAM measured
Under load. Upload 20 generated 4000x3000 JPEGs (about 1 MB each, synthetic gradients and shapes, no people) through the API as the admin user, then wait until every Immich background queue (thumbnails, metadata, CLIP smart search, face detection, ...) is empty. The machine-learning container really runs; the images contain no faces, so face recognition has nothing to find. Peak and mean are taken from the first upload until the queues are idle.
Per-container figures for Immich
ContainerIdle RAMPeak RAM (under load)Image, compressed (download)Image, unpacked (disk)
database439 MiB449 MiB205 MB989 MB
immich-machine-learning163 MiB1.8 GiB342 MB1.4 GB
immich-server757 MiB1.1 GiB617 MB2.5 GB
redis5 MiB6 MiB44 MB167 MB
Total1.3 GiB3.3 GiB1.2 GB5.0 GB
Total RAM after the stack became healthy (10 s averages)
Total RAM across all containers in 10 s averages, from the moment the stack was healthy until the end of the idle window (210 s). Highest 10 s average 2.1 GiB at 70 s, last 1.3 GiB at 210 s. Dashed line: the stack counted as settled at 176 s.

0 s to 210 s after healthy; the line spans 0 to 2.1 GiB. Dashed line: settled.

Timeline as a table
Total RAM, 10 s average ending at each time, every 30 s after healthy
Seconds after healthyTotal RAM
302.1 GiB
602.1 GiB
902.1 GiB
1201.7 GiB
1501.3 GiB
1801.3 GiB
2101.3 GiB

RAM is MiB (220 bytes); image sizes are MB (106 bytes). Per-container peaks happen at different moments, so they do not add up to the total peak.

Lab notes (5)
  • The .env adds IMMICH_HOST=0.0.0.0: the lab host has no IPv6, and the ML container binds [::] by default and crash-loops without it.
  • Compose follows the Immich release file with named volumes instead of bind mounts (see the comments in the compose file).
  • Hardware acceleration (GPU/iGPU) is not tested: the lab host has none.
  • helper image curlimages/curl:latest was pulled from mirror.gcr.io/curlimages/curl:latest because Docker Hub refused the pull
  • Run behind a TLS-intercepting proxy: an extra CA bundle was mounted read-only into every container and the SSL_CERT_FILE-style variables pointed at it (lab-only override, NOT in the published compose). It changes outbound TLS trust, nothing else.

Verified compose file

Four services: immich-server, immich-machine-learning, redis and database. The two Immich images are pinned to v3.2.4, Postgres is pinned to Immich’s tested image tag and Valkey is pinned by digest. Only port 2283 is published. The comments in the file are the lab’s.

compose.yamlVerified 2026-10-02 · Immich v3.2.4Download compose.yaml for Immich
# Verified compose for Immich, tested 2026-10-02 by the SelfHostBench lab.
# Lab host: Intel(R) Xeon(R) Processor @ 2.10GHz, 4 vCPU, 15.7 GiB RAM, x86_64, Docker 29.6.2.
# Floating image tags below were replaced by the version resolved at test time;
# the registry digest is in the comment on each image line.
# Measurements: immich.json (schema 2).
# Companion env file: immich.env (save it as .env next to this file).
# Secrets are not published: the lab's values are public. In immich.env:
#   DB_PASSWORD=CHANGE_ME_DB_PASSWORD. Replace it with your own before the first start.

# Immich: self-hosted photo and video library with machine-learning search.
#
# Based on the compose file of the current Immich release:
#   https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
# Changes from the release file (everything else is as shipped):
#   * the upload and database folders are named volumes instead of the ./library and
#     ./postgres bind mounts, so the stack can run on a remote Docker host and `down -v`
#     leaves nothing behind. Use bind mounts (UPLOAD_LOCATION / DB_DATA_LOCATION in .env)
#     if you want photos in a folder you can see.
#   * the extends/hardware-acceleration comments are removed (not tested here).
# Put DB_PASSWORD and friends in the .env file next to this file.

name: immich

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:v3.2.4  # sha256:d317916b28090c33eb36b308464ea391f8b7df1d850fcfea227a39ec879718c2  (version v3.2.4, from label:org.opencontainers.image.version)
    volumes:
      # Originals, thumbnails, encoded video. This is your photo library: back it up.
      - immich-upload:/data
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    ports:
      - '2283:2283'
    depends_on:
      - redis
      - database
    restart: always
    healthcheck:
      disable: false

  immich-machine-learning:
    container_name: immich_machine_learning
    # For hardware acceleration, add one of -[armnn, cuda, rocm, openvino, rknn] to the image tag.
    image: ghcr.io/immich-app/immich-machine-learning:v3.2.4  # sha256:e16c2f166a8174901959fdf85e2e4c7bd1ebc4b37e0b6655de97c41408a260c4  (version v3.2.4, from label:org.opencontainers.image.version)
    volumes:
      # Downloaded ML models (CLIP, face detection). Safe to delete, they are fetched again.
      - model-cache:/cache
    env_file:
      - .env
    restart: always
    healthcheck:
      disable: false

  redis:
    container_name: immich_redis
    image: docker.io/valkey/valkey:9@sha256:70739f85ad2ee01a726a965584a0f94895f01b0c60b3cc8b0aeef11eaa6888cf  # version 9.1.1 (from exec); no registry tag for it could be verified, so the digest is pinned instead
    healthcheck:
      test: redis-cli ping | grep -q PONG || exit 1
    restart: always

  database:
    container_name: immich_postgres
    image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0  # sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23  (version 14-vectorchord0.4.3-pgvector0.8.1-pgvectors0.2.0, from label:org.opencontainers.image.version)
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_DB: ${DB_DATABASE_NAME}
      POSTGRES_INITDB_ARGS: '--data-checksums'
      # Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs
      # DB_STORAGE_TYPE: 'HDD'
    volumes:
      # The Immich database (albums, users, search index). Back this up with pg_dump.
      - immich-db:/var/lib/postgresql/data
    shm_size: 128mb
    restart: always
    healthcheck:
      disable: false

volumes:
  immich-upload:
  immich-db:
  model-cache:

The .env goes next to it. It has two changes from Immich’s example.env: a line for IMMICH_HOST (see the first gotcha below) and no UPLOAD_LOCATION or DB_DATA_LOCATION, because this file uses named volumes. We also left out upstream’s commented TZ line. Add one, such as TZ=America/Chicago, if you want Immich’s nightly 2:00 AM database backup to run on your local time rather than UTC.

.envVerified 2026-10-02 · Immich v3.2.4Download .env for Immich
# You can find documentation for all the supported env variables at https://docs.immich.app/install/environment-variables

# The Immich version to use. You can pin this to a specific version like "v2.1.0"
IMMICH_VERSION=v3

# Connection secret for postgres. You should change it to a random password
# Please use only the characters `A-Za-z0-9`, without special characters or spaces
DB_PASSWORD=CHANGE_ME_DB_PASSWORD

# Immich containers listen on [::] (IPv6) by default, and the machine-learning container
# crash-loops on a host whose kernel has IPv6 disabled ("Address family not supported by
# protocol"). The lab host is one of those, so listen on IPv4 only. Harmless on hosts with
# IPv6; delete the line if you like.
IMMICH_HOST=0.0.0.0

# The values below this line do not need to be changed
###################################################################################
DB_USERNAME=postgres
DB_DATABASE_NAME=immich

The DB_PASSWORD in the downloadable .env is the placeholder CHANGE_ME_DB_PASSWORD, not the password the lab ran with. Postgres accepts that placeholder as an ordinary password (we tried it on the database image), so replace it in step 4 before the first start.

IMMICH_VERSION in this .env does nothing to the image tags. The tags are written into compose.yaml, and docker compose config --images lists them from there. Immich’s upstream file reads IMMICH_VERSION from .env instead. To change versions with ours, edit the two Immich image lines.

Install steps

We ran these steps in a scratch directory on the lab host on Oct 1, 2026 (Docker 29.6.2, Compose 5.3.1). The numbers on this page come from the lab run in immich.json, not from these manual runs.

  1. Install Docker Engine with the Compose plugin. Immich’s docs say to use docker compose (not docker-compose) and, if you get errors, to install Docker Engine from Docker’s official repository instead of a distro package (Immich install docs).

  2. Make a folder.

    mkdir immich-app
    cd immich-app
  3. Save the two files above as compose.yaml and .env in that folder. The download links are on each block.

  4. Replace the placeholder database password (CHANGE_ME_DB_PASSWORD in the downloaded .env) before the first start. We ran this command on a copy of the published file: the line then held 32 hex characters.

    sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=$(openssl rand -hex 16)/" .env
  5. Start the stack. The first start downloads the images: 1,208 MB compressed in total (table below).

    docker compose up -d
  6. Check that all four containers are healthy, then ping the server.

    docker compose ps --format 'table {{.Name}}\t{{.Status}}'
    curl -s http://localhost:2283/api/server/ping

    Our output, 50 seconds after the start:

    NAME                      STATUS
    immich_machine_learning   Up 50 seconds (healthy)
    immich_postgres           Up 50 seconds (healthy)
    immich_redis              Up 50 seconds (healthy)
    immich_server             Up 50 seconds (healthy)
    {"res":"pong"}

    The ping answered at 20 seconds, while docker compose ps still showed health: starting. Wait for four healthy rows.

  7. Open http://<host>:2283 in a browser. Immich’s post-install page says the first user to register becomes the admin. Not run in our lab: the lab created the admin through the API (setup_admin.py in the lab repo).

Measured resources

Source: immich.json, run Oct 2, 2026 on the lab host (4 vCPU Intel Xeon @ 2.10GHz, 15.7 GiB RAM, Docker 29.6.2). Idle is the mean of 16 samples over 30 s, taken after the stack’s total memory had settled (176 s after the stack was healthy; rule on the methodology page) and before any upload. Startup peak is the highest total between healthy and settled. Peak is the highest value seen while the load ran. The load was 20 generated 4000x3000 JPEGs (23.1 MB in total) uploaded through the API, then a wait until every Immich job queue was empty. That took 42.0 s, with no failed jobs, and a smart search for the images returned all 20.

Container Idle RAM, settled (MiB) Startup peak (MiB) Peak RAM under load (MiB) Image compressed (MB) Image unpacked (MB)
immich-server 757 1,631 1,078 616.7 2,480
immich-machine-learning 163 164 1,880 342.0 1,390
database (Postgres) 439 507 449 205.3 989
redis (Valkey) 5 6 6 44.3 167
Stack, highest simultaneous total 1,364 2,246 3,409 1,208.3 5,026

The stack peak is the highest sum of all four containers at one instant, so it is lower than the sum of the per-container peaks (3,412 MiB). CPU for the same run:

Container Idle CPU, mean (% of one core, lab host) Load CPU, mean (% of one core, lab host) Load CPU, max (% of one core, lab host)
immich-server 0.5 19 288
immich-machine-learning 2.3 103 383
database (Postgres) 0.00 0.8 4.6
redis (Valkey) 0.2 0.5 1.1
Stack 3.0 123 388

What the numbers say:

What we did not measure: libraries of thousands of photos or any video transcoding, face recognition on real faces (the generated images have none), hardware acceleration (the lab host has no GPU), and ARM hosts. A real import will use more than our 20-photo run.

Gotchas, upgrade and backup

The machine-learning container crash-loops without IPv6

On our lab host, whose kernel has no IPv6, the first start with Immich’s default settings left immich_machine_learning restarting. The server stayed healthy and answered the ping, so the stack looked fine apart from one container. It kept restarting for as long as we watched. The log (docker compose logs --no-color immich-machine-learning) repeated this:

ERROR    connection to ('::', 3003) failed: [Errno 97]
         Address family not supported by protocol

We ran Immich’s own release file with IMMICH_VERSION=v3.2.4 for this. Adding IMMICH_HOST=0.0.0.0 to .env and running docker compose up -d again fixed it: all four containers healthy, zero restarts. The environment variable page lists IMMICH_HOST (“listening host”, default 0.0.0.0) for both the server and machine learning, yet our log shows the container trying ::, so set it explicitly. That is why the .env above carries the line. It makes the containers listen on IPv4 only. Not tested in our lab: a host with working IPv6, or an IPv6-only host.

The database password is read once

Immich’s docs say to replace the default postgres password with a random one using only A-Za-z0-9; our downloadable .env carries a placeholder there. Do it before the first start (step 4). We changed DB_PASSWORD in .env on a running stack and ran docker compose up -d. The server could not start: password authentication failed for user "postgres". Putting the old value back and running docker compose up -d again brought the stack back. The database kept the password it was created with, so a later change in .env only changes what the server sends. The compose file does not publish port 5432, so the database is reachable only from the other containers.

Named volumes and bind mounts

Our file keeps photos, the database and the model cache in three named volumes (immich_immich-upload, immich_immich-db, immich_model-cache). The immich_ prefix is the project name, which the file fixes with name: immich, so it does not depend on your folder name. Setting COMPOSE_PROJECT_NAME or -p overrides it, so run docker volume ls if you did. Immich’s release file uses two bind mounts, UPLOAD_LOCATION and DB_DATA_LOCATION, which Immich’s example.env sets to ./library and ./postgres. Both work.

The difference matters for docker compose down -v. Docker removes the named volumes declared in the compose file with -v (docker compose down). With our file, down -v removed all three volumes, photos included. With upstream’s bind-mount file, it removed only immich_model-cache. Use plain docker compose down on a stack you want to keep. If you edit our file to use bind mounts, change the two volume lines yourself: we ran upstream’s bind-mount file, not an edited copy of ours.

Update safely

Immich’s upgrade page says to read the release notes and account for breaking changes before upgrading, and that breaking changes are posted in the project’s GitHub discussions. It also says the server is only compatible with the matching major version of the mobile app and that mobile clients should be upgraded first. It states that downgrading is not supported, so the database backup below is your only way back. Read the notes for the version you move to on the releases page. The v3.2.4 notes describe a patch. Compare the image lines in your compose.yaml with the release’s own docker-compose.yml too: the upgrade page describes a past database migration to VectorChord, so the database image can change between releases.

The steps, in order:

  1. Back up the database (next section).

  2. Change the tags of the two Immich images in compose.yaml. Our run went from v3.2.2 to v3.2.4, so replace both version numbers with yours:

    sed -i 's/:v3\.2\.2/:v3.2.4/' compose.yaml
  3. Pull and restart.

    docker compose pull
    docker compose up -d
  4. Confirm the version.

    curl -s http://localhost:2283/api/server/version

    The output was {"major":3,"minor":2,"patch":4,"prerelease":null}, and the admin account created on v3.2.2 could log in. Only the server and machine-learning containers were recreated.

If you use Immich’s own file, the version is IMMICH_VERSION in .env, and the documented update is docker compose pull && docker compose up -d. Not run in our lab as an update on that file. We pin an exact tag rather than v3: Immich’s docs offer v3 or an exact version, and a major-version tag can point at a newer release the next time you pull.

Back up and restore the database

Immich’s backup page says the database dump holds only metadata, so you also need the upload folder, and to back up the database first and the filesystem second. It also says Immich writes automatic database dumps into UPLOAD_LOCATION/backups (daily at 2:00 AM, keeping 14, by default). With our file that folder sits in the same volume as your photos, so it does not protect against losing the volume.

Dump the database and copy the upload volume out of the running server container. postgres and immich are the defaults of DB_USERNAME and DB_DATABASE_NAME in .env; change them here if you changed those:

mkdir -p backups
docker exec immich_postgres pg_dump --clean --if-exists --dbname=immich --username=postgres | gzip > backups/immich-db.sql.gz
docker cp immich_server:/data ./backups/immich-upload

The documented command uses docker exec -t. In our run, -t added a carriage return to every line of the dump (238,712 of them). That dump still restored with exit code 0, but a fresh dump of the restored database differed from the original in 26 trigger-function bodies, which now contained carriage returns. No table data differed. Without -t there were none, so we use the command without it. docker cp is fine for a small library; for a large one use a backup tool.

The restore is Immich’s documented procedure with three changes. The documented first line is docker compose down -v, which would delete the upload volume in our file, so we use docker compose down and remove only the database volume. We drop the documented docker compose pull because the images are pinned. The restore needs a database that no Immich server has touched, so the server container is created but not started until the restore finishes.

Warning: docker volume rm immich_immich-db permanently deletes the current database. Run it only after you have checked that backups/immich-db.sql.gz exists and is not empty. If the volume does not exist, as on a new machine, the error is harmless.

docker compose down
docker volume rm immich_immich-db
docker compose create
docker start immich_postgres
sleep 10
gunzip --stdout backups/immich-db.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 exec -i immich_postgres psql --dbname=immich --username=postgres --single-transaction --set ON_ERROR_STOP=on
docker compose up -d

psql exited 0, all four containers came back healthy, and the restored server accepted the admin login created before the dump. The server statistics still counted the photo we had uploaded before the dump. We ran the full cycle on v3.2.4 on Oct 1, 2026, with a stack holding one admin account and one uploaded image.

The database dump holds no photos. To restore them as well, for example after docker compose down -v or on a new machine, copy the upload backup into the server container after docker compose create and before docker compose up -d:

docker cp ./backups/immich-upload/. immich_server:/data

We ran this after docker compose down -v had removed all three volumes, with the database restored from the dump as above. The uploaded photo’s original and thumbnail then loaded from the API.

The same check, a restore into a fresh volume followed by a login that must work, is how we tested a SQLite password vault behind a reverse proxy in Vaultwarden Docker Compose.

Where to run it

  • Hardware. Immich idled at 1.3 GiB and peaked at 3.3 GiB under load in our lab. Plan for at least 8 GB of RAM. See Immich RAM requirements, measured.
  • VPS. The smallest plan that runs this comfortably has 8 GB of RAM, plus disk for the images (5.0 GB unpacked) and your data.
  • Keep it safe. Back up the data volume before you rely on it, and put the app behind a reverse proxy with HTTPS before you expose it to the internet.

How we size: recommended RAM is the highest reading we measured (under load or during startup) times 1.5, rounded up to the next of 1, 2, 4, 8, 16 or 32 GB. The extra half covers the operating system, Docker, file cache and spikes our sampling can miss. Your own data and extra apps need more.

Update log

  • The downloadable .env no longer carries the lab's database password: DB_PASSWORD is the placeholder CHANGE_ME_DB_PASSWORD, to be replaced in step 4. The lab ran with the old value; measurements are unchanged.
  • Linked the new Vaultwarden Docker Compose page from the backup and restore section.
  • Re-tested under the new idle-RAM method (we now wait for memory to settle). Idle RAM is 1,364 MiB, was 2,162; peak 3,409 MiB, was 3,324. Compose file re-verified, unchanged.
  • Linked the new Immich RAM requirements page; replaced the untested 4 GB and model-unload notes with its results.
  • First published. Compose file verified on Immich v3.2.4; update, backup and restore steps run on the same day.