SSD Nodes Learn 🎉 VPS from $4.99/mo
Guides Matt ConnorBy Matt Connor

How much RAM and disk does Immich need?

Immich documents 6 GB of RAM as its minimum. Here is what the server, Postgres, Redis and machine learning each need, and how to run it on 4 GB.

How much RAM does Immich need?

Immich asks for 6 GB of RAM (random access memory) as its documented minimum and 8 GB as its recommendation, on 2 CPU cores at the low end and 4 for a comfortable install. That figure covers the whole stack, because Immich is four containers rather than one application. Browsing a library that is already imported is cheap. The memory goes on importing, and most of it goes on one container you can switch off.

ChartImmich documented hardware requirements, August 2026
The data behind this chart
[
  {
    "label": "Documented minimum",
    "ram_gb": 6,
    "cpu_cores": 2
  },
  {
    "label": "Documented recommended",
    "ram_gb": 8,
    "cpu_cores": 4
  }
]

Those are the published figures from the Immich requirements page as of August 2026. They are a sizing recommendation, not a check the software runs at startup. Immich starts on less. What changes on a smaller server is which background jobs finish, and what an import does when memory runs out.

One genuine hard limit exists. Immich version 3 and later needs an x86-64-v2 CPU on amd64 hosts, which covers most processors sold since roughly 2012. On older hardware the container fails to start rather than running slowly.

If the install is still ahead of you, start with the full Immich install on a VPS with Docker Compose and come back here to size the box.

Where the memory goes: four containers

The official Compose file starts four services. Each has a different memory shape, so one total number hides the useful part.

immich-server serves the web interface and the API, and it also runs the background job workers. Two workers live inside that single container. api answers requests from the browser and the mobile app. microservices runs the queues, including thumbnail generation and video encoding. The variables IMMICH_WORKERS_INCLUDE and IMMICH_WORKERS_EXCLUDE split those two into separate containers, which is how you give the noisy half its own memory limit without capping the half that serves your photos.

database is a PostgreSQL 14 image with the VectorChord extension built in. It holds every piece of metadata and one search vector per asset. The Immich docs give this service the only explicit floor in the stack: if you apply Docker resource limits, the database needs at least 2 GB. The same page says the database must sit on local SSD storage and never on a network share of any kind, because vector and index lookups are small random reads, so a network volume turns each one into a round trip. If a plan choice hangs on that, the difference between NVMe and SATA SSD storage on a VPS matters more here than anywhere else in this stack.

redis runs the Valkey image and holds the job queues. It is the smallest of the four by a wide margin, because it stores job records rather than photo data.

immich-machine-learning is the service that decides your plan size. It loads models for smart search, face detection and text recognition, and a loaded model stays resident in memory. MACHINE_LEARNING_MODEL_TTL defaults to 300, so a model is dropped after five minutes with no requests and read back from the /cache volume on the next one. During a bulk import there is never a five minute gap, so the models stay loaded from the first asset to the last.

What changes during an import

An idle Immich is quiet. Imports are where small servers fall over, because uploading one asset queues a chain of jobs and several queues run at the same time.

Metadata extraction reads the file header and is light. Thumbnail generation is heavier. Immich produces three thumbnail outputs per asset, a blurred thumbhash placeholder, a WebP preview and a JPEG thumbnail, plus one more thumbnail for every detected face. Each of those jobs decodes an image, and job concurrency decides how many decode at once. Concurrency is the multiplier that turns a small per job cost into a server wide one, which is why the Immich FAQ names it as the first thing to lower on a constrained machine. Set the concurrency for the heavy queues to 1 under Administration, Settings, Job Settings.

Video assets add transcoding. Each transcode job is a separate FFmpeg process with its own memory, and it will use every CPU thread you allow it.

Smart search sends every new asset to the machine learning container to compute one embedding vector. Face detection runs a second model over the same image. On the first import of an existing photo library, both of those queues run over every asset you own, for hours. That is the worst moment for memory on the whole install, and it happens once.

Why face and object recognition need the most RAM

Face handling is two jobs. Face detection runs a model in the machine learning container and finds the boxes. Facial recognition then groups those detections into people, and that step queries the vector index in Postgres. So a large library pushes on both services in turn: the model container while detection runs, then the database while grouping runs.

Four settings change what the machine learning container holds.

  • The face model. Immich ships buffalo_l by default, and the FAQ recommends buffalo_s on a small server. It is a smaller model, so it occupies less memory and runs faster, at the cost of accuracy on small or side-on faces.
  • The worker count. MACHINE_LEARNING_WORKERS defaults to 1. Each worker is a separate process that loads its own copy of the models, so raising it to 2 roughly doubles resident model memory. Leave it at 1 unless you have RAM to spare.
  • The batch size. MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION caps how many faces are processed at once. A batch is held in memory together, so a group photo with forty faces costs more than a portrait.
  • Which model types run at all. Smart search, face detection and text recognition each load their own models. Turning off the ones you do not use, under Administration, Settings, Machine Learning Settings, removes their memory for good rather than between imports.

There is also MACHINE_LEARNING_MODEL_ARENA, documented as pre-allocating CPU memory to avoid fragmentation and on by default. Change it last. Its effect depends on the memory allocator underneath, so the only honest way to judge it is to watch docker stats before and after.

Three worked profiles: 2 GB, 4 GB and 8 GB

ChartCompose memory limits that fit each server size, in MB
The data behind this chart
[
  {
    "label": "2 GB VPS",
    "server_limit_mb": 768,
    "db_limit_mb": 768,
    "ml_limit_mb": 0,
    "redis_limit_mb": 128,
    "notes": "machine learning container removed"
  },
  {
    "label": "4 GB VPS",
    "server_limit_mb": 1024,
    "db_limit_mb": 1280,
    "ml_limit_mb": 1024,
    "redis_limit_mb": 192,
    "notes": "machine learning on, job concurrency 1, buffalo_s"
  },
  {
    "label": "8 GB VPS",
    "server_limit_mb": 2048,
    "db_limit_mb": 2048,
    "ml_limit_mb": 2560,
    "redis_limit_mb": 256,
    "notes": "everything on at default settings"
  }
]

Read those as limits to type into Compose, not as measurements of what Immich uses. A limit is a ceiling. It does not reserve anything, and it does not make a service smaller. It decides which service the kernel kills when the box runs out, and that is a decision better made by you than by the kernel's own scoring.

The 2 GB box: remove the machine learning container

2 GB sits below the documented minimum of 6 GB, so this is a compromise and worth naming as one. Comment out the whole immich-machine-learning service in docker-compose.yml, or leave it running and disable every model under Administration, Settings, Machine Learning Settings. Removing the container is the stronger option, because a disabled model still leaves a Python process resident.

You keep uploads, albums, sharing, mobile backup, thumbnails, and search by date, place and filename. You lose search by description, automatic grouping of faces into people, and text recognition inside images.

The four limits add up to about 1.7 GB, which leaves the host roughly 300 MB. Note that 768 MB for the database is under the documented 2 GB floor. That is exactly the compromise 2 GB forces, and it is why Postgres is the service most likely to be killed here.

What breaks first is the import, not the browsing. A library in the low tens of thousands of photos browses acceptably once it is in, because serving a page is a metadata query plus a file read. A video heavy import on the same box will swap, because a transcode and a thumbnail queue want the memory at the same moment. Set every heavy queue to concurrency 1 and add a swap file.

The 4 GB box: machine learning on, one job at a time

4 GB is the smallest size where face and object recognition are worth switching on. Cap the machine learning container at 0 MB, switch facial recognition to buffalo_s, and set job concurrency to 1 for thumbnail generation, face detection and smart search.

The first pass over an existing library will run for many hours, and on a large library for more than a day. That is a CPU limit rather than a memory one, so more RAM will not shorten it.

What breaks first here is the machine learning container during that first bulk pass. Uncapped, it grows while a transcode job also grows, and the kernel kills the larger of the two. You see Exited (137) in docker ps -a and a restarted container, with the queue quietly further behind than when you looked last.

The 8 GB box: the documented recommendation

8 GB with 4 cores matches what Immich recommends, and everything runs at default settings: smart search, face detection, text recognition and transcoding, at default concurrency. Libraries past a hundred thousand assets are comfortable here, and the pressure shifts from memory to disk speed, because the vector index and the metadata queries are what the database is doing all day.

Set the limits anyway. On a box with room, the limits stop one runaway queue from taking the database down with it. If you are pricing this against the smaller options, what a VPS actually costs by memory tier usually makes the 8 GB plan the cheapest way to stop tuning.

How to cap memory per service with Compose limits

Do not edit docker-compose.yml for this. That file is replaced every time you upgrade with wget. Put the limits in docker-compose.override.yml next to it, which docker compose merges automatically.

services:
  immich-server:
    deploy:
      resources:
        limits:
          memory: 1024M
  immich-machine-learning:
    deploy:
      resources:
        limits:
          memory: 1024M
          cpus: '1.5'
  database:
    deploy:
      resources:
        limits:
          memory: 1280M
  redis:
    deploy:
      resources:
        limits:
          memory: 192M
docker compose up -d
docker stats --no-stream

docker stats should now show your ceiling in the MEM USAGE / LIMIT column instead of the host's total memory. If the limit column still reads the full host size, the override file was not picked up: check the file name and run docker compose config to see the merged result.

A limit that is too low converts a slow service into a dead one, so raise it if a container starts cycling. There is more on the mechanics in setting memory limits per service in Docker Compose, including why deploy works outside Swarm with Compose v2.

How to turn off or move the machine learning container

On a small server, moving this container elsewhere is the single biggest change you can make. Immich supports running it on another machine. Create this file on the second host, which can be a desktop that is only on in the evenings:

name: immich_remote_ml
services:
  immich-machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    volumes:
      - model-cache:/cache
    restart: always
    ports:
      - 3003:3003
volumes:
  model-cache:
docker compose up -d
curl -s http://localhost:3003/ping

Then go to Administration, Settings, Machine Learning Settings in the web interface, click Add URL, and enter http://<host>:3003. Keep the version on both hosts the same, because the Immich docs warn that version mismatches between the two cause bugs and instability.

That port carries your photos to the other machine unencrypted, so keep it on a private network or run it over a WireGuard tunnel between the two hosts. Never expose 3003 to the internet.

If the whole idea of a resident model container is the problem, that is also a fair reason to compare how PhotoPrism and Immich differ in what they run at rest before committing to a plan size.

How much disk does an Immich library need?

There is no single multiplier, because four different things grow at four different rates. Here is the arithmetic for a library of 50,000 photos and 500 short videos.

ChartWorked disk estimate: 50,000 photos and 500 videos
The data behind this chart
[
  {
    "label": "Originals: 50,000 photos at 4 MB",
    "gb": 200
  },
  {
    "label": "Originals: 500 videos at 120 MB",
    "gb": 60
  },
  {
    "label": "Thumbnails and encoded video at 15%",
    "gb": 39
  },
  {
    "label": "Postgres database",
    "gb": 3
  },
  {
    "label": "Machine learning model cache",
    "gb": 2
  }
]

The 200 GB of photos and 60 GB of video are assumptions. Replace them with your own averages before you buy anything, because video decides this number: one minute of phone video is larger than a hundred photos.

find /srv/immich/upload -type f -printf '%s\n' \
  | awk '{n++; s+=$1} END {printf "%d files, %.1f MB average\n", n, s/n/1048576}'

The 39 GB row is the only published ratio Immich gives: generated thumbnails and transcoded video add 10 to 20 percent to the size of the library on average. It is a range because it depends on how many of your assets are video that needs re-encoding for browser compatibility. A library of JPEGs lands near the bottom of that range.

The database is 3 GB, and that is close to a fixed cost. Immich documents database files as typically 1 to 3 GB, because they hold metadata and search vectors rather than pixels. The model cache is 2 GB and grows if you enable several models or test different ones. The FAQ flags this volume as a space consumer for exactly that reason.

The five rows add up to a little over 300 GB, so a 500 GB volume leaves room to grow and a 250 GB volume does not. Watch the split with:

grep UPLOAD_LOCATION .env
du -sh /srv/immich/*

Six folders live under UPLOAD_LOCATION. upload and library hold the originals, thumbs holds previews and face thumbnails, encoded-video holds re-encoded copies, profile holds avatars, and backups holds automatic database dumps. Only upload, library and profile are irreplaceable, since everything else regenerates from them.

Two things surprise people. Deleted assets go to the trash first and keep their space until the trash is emptied, so a big cleanup frees nothing on the day you do it. And a database dump is metadata only, so it is worthless without the files:

docker exec -t immich_postgres pg_dump --clean --if-exists \
  --dbname=immich --username=postgres | gzip > /srv/backups/immich-dump.sql.gz

Pair that with a file level copy of the originals to somewhere off the server, which is what restic backups from a VPS to off-server storage are for.

Transcoding uses CPU, not RAM

Adding RAM will not make transcoding faster. Immich transcodes with FFmpeg, and on a plain VPS every frame is decoded and encoded by the CPU. Even where hardware acceleration is available, Immich documents that only encoding is accelerated, so the CPU still does software decoding and tone mapping.

Hardware acceleration needs the extra hwaccel.transcoding.yml Compose file plus a device to pass through, using NVENC, Quick Sync, RKMPP or VAAPI. Most VPS plans give you none of those, so plan for CPU.

The practical setting is thread count. Under Administration, Settings, Video Transcoding Settings, a thread value of 0 means all cores, which lets one video freeze the web interface on a 2 core plan. Set it to 1 or 2 there, as the Immich FAQ suggests, and a transcode becomes slow instead of disruptive.

Why a swap thrashing import looks like a hang

This is the failure people misread most often. When Immich runs out of memory there are two outcomes, and only one of them looks like a failure.

Without swap, the kernel kills a process. The container restarts within seconds, so from the browser the job queue simply stalls and then resumes. The evidence is in docker ps -a:

docker ps -a --filter name=immich
docker inspect immich_machine_learning | grep -i oomkilled
sudo dmesg -T | grep -i -E 'out of memory|oom-kill'

Exited (137) means the process was killed with signal 9. 137 is 128 plus 9. An OOMKilled value of true confirms it was killed for memory rather than by a crash.

With swap, nothing is killed and nothing errors. The kernel starts moving pages to disk, the import slows by an order of magnitude, and the web interface stops answering within a normal timeout. Every container is running. Every health check may still pass. It looks like a hang, and people reboot the box at this point, which loses the queue progress and changes nothing.

free -m
vmstat 1 5

Sustained non zero values in the si and so columns of vmstat mean the machine is reading and writing swap continuously, which is the definition of thrashing. The free -m row for Swap used will be climbing at the same time.

Add swap anyway on a 2 GB or 4 GB box, because a slow import you can diagnose beats a killed container you cannot:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Then fix the cause. Lower job concurrency to 1, cap the machine learning container, or take it off this host. Swap buys you time to do that. It is not the answer on its own.

FAQ

Can I run Immich on a 2 GB VPS?

Yes, with the immich-machine-learning service commented out of docker-compose.yml and job concurrency set to 1. That is below the documented minimum of 6 GB, so treat it as a known compromise. You keep uploads, albums, sharing, mobile backup and search by date, place and filename. You lose search by description, automatic grouping of faces into people, and text recognition inside images. Add a 2 GB swap file so an import spike slows the server down instead of getting a container killed.

Why does my Immich import stop with no error message?

Two different causes look identical from the browser. Either a container was killed for memory, in which case docker ps -a shows Exited (137) and the container has already restarted, or the host is swapping, in which case every container is still running and everything is simply very slow. vmstat 1 5 separates them: sustained non zero numbers in the si and so columns mean swapping. Lower job concurrency for thumbnail generation, face detection and smart search in either case.

What does exit code 137 mean in the Immich logs?

137 is 128 plus signal 9, so the process was killed with SIGKILL. In practice that means a memory ceiling was hit, either the container's own limit or the host running out. Check with docker inspect immich_machine_learning | grep -i oomkilled. A value of true confirms the kernel killed it for memory, and free -m plus sudo dmesg -T | grep -i oom-kill then tells you whether it was the container limit or the whole host. The machine learning container is the usual casualty because it is usually the largest process.

How much disk space does Immich need per photo?

Budget the original file plus 10 to 20 percent. Immich documents that generated thumbnails and transcoded video increase library size by 10 to 20 percent on average, and the database itself is typically 1 to 3 GB even for a large library. Video is what actually decides your total, so measure your own average file size before choosing a plan rather than applying a multiplier to a photo count.

Do I need a GPU for Immich?

No. Every part of Immich runs on CPU. A graphics card speeds up model inference in the machine learning container and video encoding, and neither is required. Most VPS plans do not offer one. On CPU only hardware, set transcoding threads to 1 or 2, use the buffalo_s face model, and let the first bulk import run overnight.