Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Troubleshooting

Immich Database: Postgres, Vector Extensions, and the Upgrade That Bites

Sean

Platform Writer

Aug 27, 2026
8 min read

Immich stores its metadata in Postgres and its machine-learning embeddings in a vector extension, and the pairing of Postgres version to extension version is strict. Almost every stuck upgrade is an extension mismatch, not a Postgres problem.

Immich Database: Postgres, Vector Extensions, and the Upgrade That Bites

The photo library itself lives on disk. The database holds metadata, albums, users, and the vector embeddings that make face recognition and semantic search work - and it is that last part, requiring a vector extension, that turns a routine container update into an ordered migration.

Table of contents

What the database actually holds

Two distinct kinds of data, with different recovery characteristics, and knowing which is which changes how you think about backups.

Metadata and structure. Users, albums, shared links, permissions, EXIF data, file paths, and job state. This is irreplaceable in the sense that losing it means losing your organisation - the photos would still be on disk, but the albums, the sharing, and the accounts would be gone.

Vector embeddings. Numerical representations of image content and faces, used for semantic search and face recognition. These are derived data. Losing them costs you a machine-learning reprocessing run over the whole library, which is slow and CPU-intensive but not destructive.

That distinction matters when a migration goes wrong. Losing embeddings is a bad evening of reprocessing. Losing metadata is losing years of organisation. Back up the database - both parts live in it - and if you are ever choosing what to protect, protect the metadata.

The photo files themselves are on the filesystem, not in the database. They are backed up separately, and a database restore without the matching files gives you a catalogue pointing at nothing.

The vector extension situation

Immich has moved between vector extensions over its life. Older installations were commonly set up with one extension image; current versions use a different one, with the base pgvector extension as a prerequisite.

The practical consequence: the Postgres image you are running encodes both a Postgres version and a set of extension versions. Pulling a newer image can change the extension version underneath a database that is not ready for it, and Immich will refuse to start with an error naming the mismatch.

The error is at least clear about what happened - it names the extension version the database has activated and the version the Postgres instance actually offers, and identifies the situation as a downgrade. That is more helpful than most migration errors, and it tells you exactly what to fix.

There is a hard rule that follows from this: you cannot skip intermediate versions. If the migration path goes through a specific intermediate image, going directly to the latest one leaves the database in a state neither image can open. Recovering from that means restoring a backup taken before the attempt, which is why the backup is not optional.

Upgrading safely

The order matters more than any individual step.

  1. Back up the database first. Not after the containers are down, not once something looks wrong. First.
  2. Note your current versions - the Postgres image tag and the extension version the database has activated.
  3. Find the documented migration path for your starting point in the official documentation. Do not infer it.
  4. Move one step at a time, letting Immich start and complete its reindexing between steps.
  5. Verify that search and face recognition still work before moving to the next step.
# Back up before touching anything
docker exec -t immich_postgres \
  pg_dumpall --clean --if-exists --username=postgres \
  > immich-backup-$(date +%F).sql

# Check the size - a suspiciously small file means it failed
ls -lh immich-backup-*.sql

That size check catches a genuinely common failure. A dump that errored partway produces a small file, and discovering that after the upgrade destroyed your database is the worst possible time.

# What extensions does the database currently have?
docker exec -it immich_postgres psql -U postgres -d immich \
  -c "SELECT extname, extversion FROM pg_extension;"

# What image is running?
docker inspect immich_postgres --format '{{.Config.Image}}'

When the migration involves switching extensions, watch the Immich logs for the reindexing messages that confirm the vector indexes were rebuilt. Moving to the next step before that completes is how people end up in the unrecoverable state.

docker logs -f immich_server 2>&1 | grep -i -E 'reindex|vector|extension'

Using an existing Postgres server

Running Immich against a Postgres instance you already operate is possible and explicitly described as not the recommended path. The requirements are specific.

You need pgvector installed as a prerequisite, within a supported version range, and the vector extension configured in the shared preload libraries setting - which requires a Postgres restart, not a reload. Immich also expects superuser permission by default, though there is a documented path for running without it.

# Install pgvector for your Postgres major version
sudo apt install postgresql-16-pgvector

# Add the extension library to preload, then restart
# shared_preload_libraries = 'vchord.so'
sudo systemctl restart postgresql

# Verify it loaded
psql -U postgres -c "SHOW shared_preload_libraries;"

The honest advice matches the documentation: use the dedicated container unless you have a specific reason not to and are comfortable with Postgres administration. The bundled image exists precisely so the extension versions are correct together, and stepping outside it means owning that compatibility yourself through every future upgrade.

If you do run your own instance, the shared preload libraries setting is the one to get right. It requires a full restart to take effect, and a reload will appear to succeed while changing nothing - which produces a confusing failure where the configuration looks correct and the extension is unavailable.

Backups that would actually restore

A backup you have never restored is a hypothesis. For a self-hosted photo library holding years of family photos, that is not a comfortable position.

#!/usr/bin/env bash
set -euo pipefail

STAMP=$(date +%F)
OUT=/backups/immich
mkdir -p "$OUT"

# Database
docker exec -t immich_postgres \
  pg_dumpall --clean --if-exists --username=postgres \
  | gzip > "$OUT/db-$STAMP.sql.gz"

# Fail loudly on a suspiciously small dump
size=$(stat -c%s "$OUT/db-$STAMP.sql.gz")
[ "$size" -gt 100000 ] || { echo "dump too small: $size bytes" >&2; exit 1; }

# Retain 14 days
find "$OUT" -name 'db-*.sql.gz' -mtime +14 -delete

Three habits that make the difference between a backup and a backup strategy. Test a restore into a scratch database periodically - quarterly is enough to catch the problem before it matters. Keep a copy somewhere other than the machine running Immich, because a disk failure that takes the library takes local backups with it. And back up the photo files separately, since a database restore without matching files gives you an empty catalogue.

The general point applies well beyond this application. Self-hosting means the database operations - patching, backups, tested restores, extension compatibility - are yours. That is entirely doable and it is real, recurring work, and the moment it stops being enjoyable is usually the moment a managed instance starts looking reasonable.

For applications where you are hosting the service rather than a specific self-hosted product, managed Postgres or MySQL on RunxBuild covers backups, connection limits, user management, and private networking without you scheduling any of it.

How this fits the rest of the stack

The recurring cost of self-hosting is not the server, it is the database operations - backups, tested restores, and extension compatibility on every upgrade. For applications where you control the stack, managed Postgres or MySQL removes that work entirely. The RunxBuild hosting calculator shows the service, the managed database, and storage as separate line items so the trade is visible as a number.

Useful related references:

FAQ

What database does Immich use?

Postgres, with a vector extension for the embeddings that power semantic search and face recognition. The Postgres version and the extension version are tightly coupled, which is why the project ships a dedicated database image rather than expecting you to assemble one.

Why does Immich fail to start after a database image update?

Almost always a vector extension mismatch - the database has a newer extension version activated than the Postgres instance now offers, which reads as a downgrade. The error names both versions. The fix is following the documented migration path in order, not jumping to the latest image.

Can I skip intermediate versions when upgrading?

No. Migration paths go through specific intermediate images, and skipping them leaves the database in a state neither the old nor the new image can open. Recovery then requires restoring a backup taken before the attempt, which is why backing up first is not optional.

Can I run Immich against my existing Postgres server?

Yes, but the documentation describes it as not recommended. You need pgvector within a supported version range, the vector extension in the shared preload libraries setting, and a full Postgres restart for that to take effect. You then own extension compatibility through every future upgrade.

What should I back up for Immich?

Both the database and the photo files, since they live in different places. The database holds metadata, albums, users, and embeddings - the embeddings can be regenerated by reprocessing, but the metadata cannot. A database restore without matching files gives you a catalogue pointing at nothing.

#immich database#postgres vector extension#self-hosting#pgvector#database migration