Skip to content
Go back

Immich vs PhotoPrism vs Ente: Self-Hosted Photo Libraries

By SumGuy 10 min read
Immich vs PhotoPrism vs Ente: Self-Hosted Photo Libraries

You Have 80,000 Photos and No Exit Plan

Google Photos killed the free tier, Dropbox costs more per year than a Raspberry Pi, and iCloud is just Apple holding your memories hostage. You’ve been putting off the self-hosted photo solution for two years because you kept hoping this would resolve itself. It won’t.

The good news: the self-hosted photo management space has genuinely matured. The bad news: three of the best options — Immich, PhotoPrism, and Ente — make wildly different bets about what “solved” looks like, and picking the wrong one will cost you a weekend of regret.

Let’s cut through the marketing copy.


The Contenders

Immich — the new hotness. Active development, polished mobile apps, face recognition that actually works, timeline view that feels like Google Photos. Also: breaking changes every few releases and a Docker stack that requires four containers minimum.

PhotoPrism — the quiet professional. Battle-tested, more archival in feel, solid for curating a well-organized collection. Less aggressive ML, calmer release cadence. Mobile backup is not its strong suit.

Ente — the privacy absolutist. End-to-end encrypted, open source, also offers a commercial SaaS tier if you want to support the devs. Self-hosting the full stack means building Flutter apps from source. It’s real work.


Immich: Google Photos Didn’t Retire, It Just Moved Servers

If you want the Google Photos experience on hardware you control, Immich is the closest thing that exists right now. The mobile apps are polished, photo backup runs in the background, and the ML pipeline does faces, objects, and CLIP-based semantic search entirely on-prem.

The trade-off is complexity. The Compose stack has four services plus an optional external ML worker:

docker-compose.yml
services:
immich-server:
image: ghcr.io/immich-app/immich-server:v1.106.4
container_name: immich_server
volumes:
- /mnt/photos:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
- "2283:2283"
depends_on:
- redis
- database
restart: always
immich-machine-learning:
image: ghcr.io/immich-app/immich-machine-learning:v1.106.4
container_name: immich_machine_learning
volumes:
- model-cache:/cache
env_file:
- .env
restart: always
redis:
image: redis:6.2-alpine
container_name: immich_redis
restart: always
database:
image: tensorchord/pgvecto-rs:pg14-v0.2.0
container_name: immich_postgres
env_file:
- .env
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
volumes:
- pgdata:/var/lib/postgresql/data
restart: always
volumes:
model-cache:
pgdata:
.env
DB_HOSTNAME=database
DB_USERNAME=immich
DB_PASSWORD=changeme_please
DB_DATABASE_NAME=immich
REDIS_HOSTNAME=redis
UPLOAD_LOCATION=/mnt/photos

A few things that will bite you if you skip the docs:

Pin the version. Immich tags latest and moves fast. Major releases routinely include breaking database migrations. Run v1.106.4 (or whatever version you’re starting with) explicitly, not latest. When you upgrade, do it one minor version at a time and back up Postgres first.

Do not point Immich at your existing photo folder and expect it to leave files alone. In default mode, Immich manages its own upload directory. There’s an “external library” mode for watching your existing NAS share, but it’s read-only — Immich won’t move or rename your originals. Use it. If you let Immich import into its managed storage, your files end up in a date-bucketed folder structure you’ll hate when you need to dig them out manually.

ML RAM costs are real. The machine-learning container pulls CLIP for semantic search and InsightFace for face recognition. On a fresh start with nothing cached, expect 2–4 GB of RAM consumed just by the ML service while it indexes. On a machine with 8 GB total, this hurts. You can tune the model cache and run ML on a schedule rather than continuously, but plan for it.

Google Photos Takeout migration: Immich has an official CLI tool for importing Takeout archives. It handles the .json sidecar files Google attaches to preserve metadata — dates, GPS, album membership. Run it with:

Terminal window
docker run --rm -it \
-v /path/to/takeout:/import \
-e API_KEY=your_immich_api_key \
ghcr.io/immich-app/immich-cli:latest \
upload --recursive /import

It’s not magic — some photos lose their original dates if Google’s JSON sidecars are malformed — but it’s the best Takeout importer in the space.

Storage backend: Immich speaks S3. Set IMMICH_MEDIA_LOCATION to an S3-compatible endpoint and it’ll store originals in your bucket. MinIO on the same host, Backblaze B2, Wasabi — all work. For a homelab with a big NAS, local FS is usually fine and simpler to manage.


PhotoPrism: The Archivist’s Choice

PhotoPrism has been around longer, moves more deliberately, and has a fundamentally different philosophy: it’s a viewer and organizer for photos you already have, not a primary backup target.

The Compose setup is blessedly simpler:

docker-compose.yml
services:
photoprism:
image: photoprism/photoprism:231128-ce
container_name: photoprism
restart: unless-stopped
security_opt:
- seccomp:unconfined
- apparmor:unconfined
ports:
- "2342:2342"
environment:
PHOTOPRISM_AUTH_MODE: "password"
PHOTOPRISM_SITE_URL: "http://photoprism.local:2342/"
PHOTOPRISM_ORIGINALS_LIMIT: 5000
PHOTOPRISM_HTTP_COMPRESSION: "gzip"
PHOTOPRISM_LOG_LEVEL: "info"
PHOTOPRISM_READONLY: "false"
PHOTOPRISM_EXPERIMENTAL: "false"
PHOTOPRISM_DISABLE_CHOWN: "false"
PHOTOPRISM_DISABLE_FACES: "false"
PHOTOPRISM_DISABLE_CLASSIFICATION: "false"
PHOTOPRISM_DISABLE_VECTORS: "false"
PHOTOPRISM_DISABLE_RAW: "false"
PHOTOPRISM_ADMIN_USER: "admin"
PHOTOPRISM_ADMIN_PASSWORD: "changeme_please"
PHOTOPRISM_DATABASE_DRIVER: "mysql"
PHOTOPRISM_DATABASE_SERVER: "mariadb:3306"
PHOTOPRISM_DATABASE_NAME: "photoprism"
PHOTOPRISM_DATABASE_USER: "photoprism"
PHOTOPRISM_DATABASE_PASSWORD: "changeme_please"
volumes:
- /mnt/photos/originals:/photoprism/originals
- /mnt/photos/storage:/photoprism/storage
depends_on:
- mariadb
mariadb:
image: mariadb:11
container_name: photoprism_db
restart: unless-stopped
security_opt:
- seccomp:unconfined
- apparmor:unconfined
environment:
MARIADB_AUTO_UPGRADE: "1"
MARIADB_INITDB_SKIP_TZINFO: "1"
MARIADB_DATABASE: "photoprism"
MARIADB_USER: "photoprism"
MARIADB_PASSWORD: "changeme_please"
MARIADB_ROOT_PASSWORD: "changeme_please"
volumes:
- dbdata:/var/lib/mysql
volumes:
dbdata:

PhotoPrism indexes your existing directory structure and leaves files where they are. That’s the whole point. If you’ve spent years organizing /photos/2023/vacation/ the way you like it, PhotoPrism respects that. It won’t restructure anything.

What it does well: RAW format support is excellent. The indexing pipeline handles EXIF, XMP sidecars, videos, and Live Photos. The “moments” view groups by location and time in a way that feels curated rather than algorithmic.

What it doesn’t do well: Mobile backup. There’s no first-party PhotoPrism app that does background upload the way Google Photos does. You’re expected to sync your phone to a folder (via Syncthing, rclone, or your NAS’s mobile app) and let PhotoPrism index from there. For a lot of people, this is fine — it’s the same workflow they’d use for any other server. For others, it’s the dealbreaker.

Initial indexing: On a large library (50k+ photos), expect the first index to run for hours. The face recognition pass is separate and even slower. Let it run overnight and go touch grass.

S3 backend: PhotoPrism supports S3-compatible storage via the PHOTOPRISM_STORAGE_PATH setting. Originals can live on S3 if you configure it at startup — harder to change after the fact.


Ente: When the Government Is Your Threat Model

Ente is built on a different premise than the other two: your photos are encrypted on your device before upload, and the server never sees the plaintext. Zero-knowledge. This means even if someone compromises your self-hosted Ente server, they get encrypted blobs — not your photos.

The trade-off: the self-hosting story is genuinely rough.

The server side is straightforward enough:

docker-compose.yml
services:
museum:
image: ghcr.io/ente-io/server:latest
container_name: ente_server
restart: unless-stopped
ports:
- "8080:8080"
environment:
ENTE_DB_HOST: postgres
ENTE_DB_PORT: 5432
ENTE_DB_NAME: ente_db
ENTE_DB_USER: pguser
ENTE_DB_PASSWORD: pgpassword
ENTE_S3_ARE_LOCAL_BUCKETS: "true"
ENTE_S3_B2_EU_ENDPOINT: "http://minio:9000"
ENTE_S3_B2_EU_KEY: minioadmin
ENTE_S3_B2_EU_SECRET: minioadmin
ENTE_S3_B2_EU_BUCKET: ente-objects
ENTE_S3_B2_EU_REGION: eu-central-2
depends_on:
- postgres
- minio
postgres:
image: postgres:15
container_name: ente_postgres
restart: unless-stopped
environment:
POSTGRES_USER: pguser
POSTGRES_PASSWORD: pgpassword
POSTGRES_DB: ente_db
volumes:
- pgdata:/var/lib/postgresql/data
minio:
image: minio/minio
container_name: ente_minio
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- minio-data:/data
ports:
- "9000:9000"
- "9001:9001"
volumes:
pgdata:
minio-data:

Here’s where it gets real: the mobile apps you download from the App Store / Play Store are hardcoded to hit Ente’s commercial servers. To use them against your self-hosted instance, you have to build the Flutter apps yourself from the open-source repo, pointing them at your server endpoint. That’s not impossible — Flutter toolchain, dart pub get, flutter build — but it’s not a Sunday afternoon project if you haven’t done Flutter before.

Ente does have a web client (photos.ente.io) that can be pointed at a custom server endpoint in settings. For desktop use, that’s workable. For mobile, you’re either building the app yourself or waiting for Ente to ship a configurable-server build to the app stores.

What Ente excels at: If you’re self-hosting for privacy reasons — not just cost — this is your answer. E2E encryption is baked into the architecture, not bolted on. The albums, sharing, and family plan features work exactly as you’d expect once you’re up.

What it doesn’t do: On-prem ML. Because the server never sees decrypted photos, face recognition and semantic search run on-device. The app is smarter than you’d expect, but it’s not doing the heavy server-side CLIP indexing that Immich does.


Backup Discipline: The Part Everyone Skips

All three tools can eat your photos if you’re careless. Ground rules regardless of which you pick:

Back up the database separately from photos. If you restore from a backup that’s six weeks old but the photos folder is current, you have orphaned files and missing index entries. Align your backup schedule.

For Immich (Postgres + pgvector):

Terminal window
docker exec immich_postgres \
pg_dumpall -U immich | gzip > /backup/immich_$(date +%Y%m%d).sql.gz

For PhotoPrism (MariaDB):

Terminal window
docker exec photoprism_db \
mariadb-dump -u photoprism -pchangeme_please photoprism | \
gzip > /backup/photoprism_$(date +%Y%m%d).sql.gz

Test your restores. Seriously. Every quarter, spin up a fresh container with the backup dump and verify you can browse photos. “I have backups” and “my backups work” are different facts.

For Immich: back up before every upgrade. No exceptions. The DB migration is automatic and non-reversible. If something goes sideways mid-migration, you need that pre-upgrade dump.


The Verdict on Mobile Backup

FeatureImmichPhotoPrismEnte
Native iOS appYesNoYes (self-built or SaaS)
Native Android appYesNoYes (self-built or SaaS)
Background auto-uploadYesManual/third-partyYes
Works offlinePartialPartialYes (E2E)

If phone backup is your primary use case, this table basically makes the decision for you. Immich and Ente both have real mobile apps. PhotoPrism expects you to handle the sync yourself.


Migration from Google Photos Takeout

All three can ingest Takeout archives. The process:

  1. Go to takeout.google.com, select only Google Photos, export all albums
  2. Download the .zip chunks (can be 10–50 GB)
  3. Extract everything into a staging folder

Immich: Use the immich-cli Docker image. It reads the JSON sidecars and preserves dates and album structure.

PhotoPrism: Point PHOTOPRISM_ORIGINALS_PATH at your extracted folder and run photoprism index. It reads EXIF but struggles with Google’s JSON sidecars. You may need exiftool to bake dates back into the files first:

Terminal window
# Bake Google Takeout JSON metadata back into EXIF before indexing
exiftool -r -d "%Y:%m:%d %H:%M:%S" \
"-DateTimeOriginal<${SubSecDateTimeOriginal}" \
"-DateTimeOriginal<DateTimeOriginal" \
"-DateTimeOriginal<CreateDate" \
-overwrite_original \
/path/to/takeout/

Ente: Import via the desktop app or CLI. The encryption means the upload is slower — every file is encrypted client-side before transit.


The Bottom Line

If you want the Google Photos replacement with real mobile backup, active development, and ML that actually finds your dog’s face: Immich. Pin your versions, respect the external library mode, back up Postgres before upgrades. This is the one most people should run.

If you have a large existing photo archive you’ve spent years organizing and you want a viewer that respects your folder structure without trying to take over: PhotoPrism. Set expectations correctly about mobile backup — you’ll need a separate sync solution for your phone.

If end-to-end encryption is non-negotiable — you’re protecting source material, sensitive personal photos, or you just don’t trust any server including your own: Ente. Budget time to build the Flutter apps or use the web UI. The privacy model is genuinely solid.

The real talk: most homelabbers are going to pick Immich and be happy with it. The active development and polished apps make it feel like a product, not a project. Just don’t run latest in production, back up your database, and resist the urge to let it manage your originals folder unless you’re okay with its naming scheme.

Your photos deserve better than a shared folder called MISC BACKUP 2019. Pick one and run it.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
OpenRouter vs LiteLLM
Next Post
Unbound vs Technitium vs BIND

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts