SSD Nodes Learn
Guides Matt ConnorBy Matt Connor

Self-host Immich: your own Google Photos

Self-host Immich on a VPS with the official Docker Compose: server, ML search, Postgres and Redis, mobile backup, HTTPS, safe upgrades and backups.

What you are building

Immich is a self-hosted photo and video backup service — a real replacement for Google Photos. It has a phone app that uploads your camera roll in the background, a timeline, albums, face recognition and machine-learning search that finds "beach" or a person without you ever tagging anything. You run it on a VPS you own, the original files stay on your disk, and nobody scans them to sell you things.

The install is four containers from the project's own Docker Compose file. That part takes ten minutes. The rest of this guide is where the pain lives: the machine-learning container is memory-hungry on a small box, originals eat disk fast, the mobile app refuses a plain-HTTP server, and Immich ships breaking changes often enough that a careless docker compose pull can leave your database unable to start. Treat those four things seriously and Immich is rock-solid. Ignore them and you will lose a weekend.

Prerequisites, and the honest gotchas

  • RAM: the official docs say 6 GB minimum and 8 GB recommended — treat 4 GB plus swap as the absolute floor. The immich-server and Postgres containers are modest. The immich-machine-learning container is the hog — it loads CLIP and face-recognition models into RAM to build search indexes, and on a 2 GB box the kernel kills it. Add swap even if you have 4 GB.
  • Disk: size it for your whole library, then some. Your originals are copied in full, plus Immich generates thumbnails and preview images (roughly 10–20% on top). A 200 GB photo collection wants a 300 GB volume. Postgres is small by comparison.
  • CPU: any modern KVM VPS is fine, but ML on CPU is slow. Smart-search indexing of a big import can run for hours in the background. That is normal; it does not need a GPU.
  • A domain name pointed at the VPS. The mobile app strongly prefers an HTTPS endpoint, and you want a reverse proxy in front. This is the same shape of setup as a self-hosted Nextcloud instance with Docker, TLS and backups — Immich is the photos counterpart to that files server.
  • Docker and the Compose plugin installed. On a fresh Ubuntu 24.04 box: sudo apt update && sudo apt install -y docker.io docker-compose-v2.

Step 1: Add swap before anything else

The single most common Immich failure on a small VPS is the ML container getting OOM-killed. Give the kernel somewhere to breathe first.

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

free -h should now show a Swap: line of 4.0Gi. This will not make ML fast, but it stops the container dying mid-index on a 4 GB machine.

Step 2: Grab the official compose and env — use theirs, not a copy

Immich pins its service versions and, critically, its database image inside the files it ships. Do not paste a compose file from a blog (including this one) as your source of truth. Download the release assets:

sudo mkdir -p /opt/immich && cd /opt/immich
sudo wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
sudo wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env

These come from the tagged release, so the image references match. The compose file defines four services, and it helps to know what each one is before you touch anything:

  • immich-server (ghcr.io/immich-app/immich-server, container immich_server) — the API and web UI, listening on port 2283. It mounts your uploads at /data.
  • immich-machine-learning (ghcr.io/immich-app/immich-machine-learning, container immich_machine_learning) — CLIP search and face recognition. Caches downloaded models in a model-cache volume. This is the memory-hungry one.
  • database (container immich_postgres) — Postgres with the VectorChord vector extension, which powers similarity search. The image tag is pinned by digest right in the compose file, for example ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:.... Older setups used pgvecto.rs; support for it was removed in Immich v3.0, so anything you install today is VectorChord. Never hand-edit this tag.
  • redis (container immich_redis) — a Valkey/Redis instance for job queues.

Step 3: Configure .env — where your photos and database live

Open .env and set four things. Everything below the marked line stays as-is.

# Where original uploads are stored on the host
UPLOAD_LOCATION=/opt/immich/library

# Where the Postgres data lives. NEVER put this on an NFS/network share.
DB_DATA_LOCATION=/opt/immich/postgres

# "v3" is a floating tag that tracks the latest v3.x. Pin a full tag like
# v3.0.2 instead — then you upgrade on purpose, not by surprise.
IMMICH_VERSION=v3.0.2

# Change this to a long random string. Letters and digits only.
DB_PASSWORD=REPLACE_WITH_A_LONG_RANDOM_STRING

# Set your timezone so timestamps and "on this day" line up
TZ=Europe/London

###################################################################################
DB_USERNAME=postgres
DB_DATABASE_NAME=immich

Two rules that save you grief. UPLOAD_LOCATION should point at your big disk — if you attach a data volume later, set this to its mount path from the start, because moving it after the fact means moving thumbnails and updating asset paths. And DB_DATA_LOCATION must be on local disk: Postgres on an NFS or SMB share corrupts, and the docs say so in plain words. If you use only letters and digits in DB_PASSWORD you avoid a class of connection-string escaping bugs.

Step 4: First run and creating the admin user

cd /opt/immich
sudo docker compose up -d
sudo docker compose ps

A correct result is four containers, all running and eventually healthy:

NAME                      STATUS
immich_machine_learning   Up (healthy)
immich_postgres           Up (healthy)
immich_redis              Up (healthy)
immich_server             Up (healthy)

The first up pulls several gigabytes of images, so give it time. Watch progress with sudo docker compose logs -f immich-server; the server logs that it is listening on port 2283 once it is ready. Now open http://YOUR_SERVER_IP:2283 in a browser. The first visit shows a Getting Started wizard — the first account you create is the admin. Set a strong password; this account owns the server settings, user management and the ML configuration you will need later.

Step 5: The mobile app and background backup

Install "Immich" from the App Store or Play Store. On the login screen it asks for a Server Endpoint URL. Enter the full URL including the scheme, for example https://photos.example.com (the app appends /api itself). Log in with the account you just made, then open the app's Backup screen, pick the albums to protect (usually Camera and Screenshots), and enable Background backup. iOS background backup is throttled by the OS — foreground uploads always run, background ones happen when the OS allows.

This is exactly where people get stuck, so read Step 6 before you fight the app.

Step 6: HTTPS via a reverse proxy — and the full-URL rule

The mobile app really wants HTTPS. Put a reverse proxy in front of port 2283 and terminate TLS there. If you already run several containers, Traefik with automatic TLS for multiple Docker apps is the tidiest option — one label block routes photos.example.com to the immich-server container and fetches the certificate for you. If you prefer nginx, the Let's Encrypt with Certbot and nginx guide gets you a certificate and a proxy_pass http://127.0.0.1:2283; block. One proxy setting matters for Immich: raise the upload size limit, because phone videos are large. In nginx that is client_max_body_size 50000M; inside the server block — the default 1 MB rejects video uploads with 413 Request Entity Too Large.

The rule the app enforces: the endpoint must be reachable and, in practice, must be HTTPS. http:// endpoints, or a direct IP with the port left off, are where "the app cannot reach the server" comes from — covered as a named failure below.

Step 7: External libraries vs uploads — importing an existing photo tree

There are two ways photos get into Immich, and they are not the same thing.

  • Uploads are assets Immich owns. The app or web uploader copies the file into UPLOAD_LOCATION. Immich can rename, move and delete them.
  • External libraries are read-only imports of files that already sit in a folder on your server — an old Pictures tree, a NAS export. Immich indexes them in place and shows them in the timeline, but never modifies or deletes the originals.

To import an existing tree, mount it read-only into the server container. Edit docker-compose.yml under immich-server: and add a volume:

  immich-server:
    volumes:
      - ${UPLOAD_LOCATION}:/data
      - /etc/localtime:/etc/localtime:ro
      - /srv/photos:/mnt/media/photos:ro

The :ro guarantees Immich can never touch the originals. Recreate the container with sudo docker compose up -d, then in the web UI go to your avatar → Administration → External Libraries → Create Library, pick the owning user, click Add under Folders, and enter the container path — /mnt/media/photos, not the host path /srv/photos. Click Scan. Using the host path instead of the container path is the number-one external-library mistake; the scan finds nothing and reports zero assets.

Step 8: The upgrade discipline Immich demands

This is the part that separates a happy Immich from a broken one. Immich ships fast and does not backport fixes or support downgrades. Blindly tracking the floating v3 tag will eventually break your database. The discipline:

  1. Pin a version. Keep IMMICH_VERSION set to a concrete tag like v3.0.2, not the floating v3 that always pulls the newest v3.x.
  2. Read the release notes every single time before upgrading. Breaking changes — especially database or vector-extension changes — are called out there. The v3.0 release is the obvious example: it removed pgvecto.rs outright, so anyone still on the old extension had to finish the VectorChord migration (introduced back in v1.133) before they could move up.
  3. Back up the database first (Step 9). Always, but doubly so when the notes mention the database.
  4. Take the new compose file too. IMMICH_VERSION only pins the server and ML images. The Postgres image is pinned by digest inside docker-compose.yml, so a version that needs a newer database extension ships a new compose file. Re-download both release assets, re-apply your .env values, then upgrade.
  5. Update your mobile clients around the same time. The server only speaks its matching major version, and the app supports the current and previous major. A server that has jumped ahead of the app shows Your app major version is not compatible with the server! on the phone until you update it, so it is safest to update the app first.

The actual commands, once you have the new files in place:

cd /opt/immich
sudo docker compose pull
sudo docker compose up -d
sudo docker image prune

Step 9: Backups — a database dump PLUS the originals, and test it

A backup of Immich is two things, and one without the other is useless. The database holds the album structure, faces, search indexes and the map from asset to file. The originals directory holds the actual photos. Restore one without the other and you get either photos with no organisation or an empty shell pointing at missing files.

Dump the database with pg_dump from inside the Postgres container — the immich database specifically, not the whole cluster:

sudo docker exec -t immich_postgres pg_dump --clean --if-exists \
  --dbname=immich --username=postgres | gzip > /opt/immich/immich-db-$(date +%F).sql.gz

Then back up UPLOAD_LOCATION — the whole /opt/immich/library tree, and in particular its library/, upload/ and profile/ subfolders — with restic, rsync or borg to another machine or object storage. Do the database first and the files second, so the dump never references a photo the file backup has not copied yet. External libraries you back up separately at their real source; Immich does not own them.

Now the part everyone skips: test the restore. A restore must run against a fresh stack whose server has never started, on a Postgres image whose vector extension is compatible with the dump — which is exactly why you never improvise the DB image tag. On a scratch box with the same compose and .env, wipe any old state, bring up only the database, then load the dump:

cd /opt/immich
sudo docker compose down -v
sudo docker compose pull
sudo docker compose create
sudo docker start immich_postgres
sleep 10
gunzip --stdout immich-db-2026-07-15.sql.gz |
  sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" |
  sudo docker exec -i immich_postgres psql --dbname=immich --username=postgres --single-transaction --set ON_ERROR_STOP=on
sudo docker compose up -d

The sed rewrite of search_path is not optional on a VectorChord database — leave it out and the restore aborts partway. When the stack comes back up with your originals in place, open the web UI: if your photos and albums are there, your backup works. If you have never run this, you do not have a backup — you have a hope.

Failure modes, with the strings you will see

The ML container is OOM-killed. sudo docker compose logs immich-machine-learning ends abruptly, docker compose ps shows it Restarting, and the exit code is 137. sudo dmesg | grep -i oom confirms it: Out of memory: Killed process ... (python3). Search and face jobs then stall. The cause is too little RAM for the models. Fixes, in order: add swap (Step 1); give the VPS more RAM; or, if you genuinely cannot, disable ML in Administration → Settings → Machine Learning Settings by turning off Smart Search and Facial Recognition — you keep backups and albums, you lose search-by-content. Removing the immich-machine-learning service from the compose file has the same effect.

Postgres refuses to start after an upgrade. The server log loops with a line like The database currently has VectorChord 0.5.3 activated, but the Postgres instance only has 0.4.2 available. This most likely means the extension was downgraded. — or, on older stacks, The pgvecto.rs extension is not available in this Postgres instance. The cause is a database image whose extension version is older than what your data was upgraded to, almost always from editing the image tag by hand or restoring a newer dump onto an older image. The fix is to use the Postgres image that matches — take the compose file from the release that matches your database, do not downgrade, and restore only onto a compatible image.

The mobile app cannot reach the server. The login screen shows a connection error / Server is not reachable after you enter the URL. Three causes: you typed http:// where the proxy only serves https://; you connected straight to the backend but left the port off, so it tried example.com (port 443) instead of example.com:2283; or the reverse proxy is not forwarding /api. Fix by entering the full https://photos.example.com URL and confirming it loads in a phone browser first. If the browser works and the app does not, the proxy is stripping the path or the certificate is self-signed — the app rejects untrusted certs.

Out of disk mid-import. Uploads start failing, thumbnails go blank, and logs show ENOSPC: no space left on device or, from Postgres, could not extend file ... No space left on device. df -h shows the UPLOAD_LOCATION volume at 100%. This is why you size disk before importing a big library. Recover by attaching a larger volume, stopping the stack, moving UPLOAD_LOCATION to it, updating .env, and starting again — or expand the existing disk if your provider allows it. Postgres can wedge if it fills up, so clear space and restart the database container before assuming corruption.

FAQ

How much RAM and disk does Immich need?

Immich's official requirements are 6 GB of RAM minimum and 8 GB recommended — 4 GB with swap is the practical floor for a small library, and configure swap either way, because the machine-learning container is the part that spikes. For disk, budget your full library size plus roughly 10–20% for generated thumbnails and previews, on local storage — never put the Postgres data directory on a network share. If you are still deciding what else to run, the guide to what to self-host in 2026 puts Immich's footprint next to other services.

Can I run Immich without a GPU?

Yes. The machine-learning container runs happily on CPU — a GPU only speeds up smart-search indexing and, with the right image variant, video transcoding. On CPU, the initial index of a large library can take hours in the background, but it does not block backups or browsing. If your box is too small for ML at all, you can disable Smart Search and Facial Recognition in the admin settings and keep everything else.

How do I safely upgrade Immich?

Pin IMMICH_VERSION to a concrete tag like v3.0.2, read the release notes before every upgrade, and back up the database first. Because the Postgres image is pinned inside docker-compose.yml rather than by IMMICH_VERSION, re-download both the compose file and example.env from your target release and re-apply your values, then run docker compose pull && docker compose up -d. Never let the version float unattended — Immich ships breaking changes and does not support downgrades.

What exactly do I back up?

Two things, together: a pg_dump of the immich database and the entire UPLOAD_LOCATION originals directory. The database holds albums, faces and the asset-to-file mapping; the directory holds the actual photos, and a restore needs both plus a database image with a compatible vector extension. Do the database dump first and the file copy second, and test the restore on a scratch box at least once — an untested backup is not a backup.

How do I import my existing photo folder?

Mount the folder read-only into the immich-server container as an extra volume (for example - /srv/photos:/mnt/media/photos:ro), recreate the container, then in Administration → External Libraries create a library and add the container path /mnt/media/photos. Immich indexes the files in place and never modifies or deletes them. The most common mistake is entering the host path instead of the container path, which makes the scan find nothing.