Back

How to Self Host Immich Without Losing a Single Photo

Immich is the best Google Photos replacement the self hosting crowd has. It also fails quietly. Here are the problems that actually bite people, and the fixes that stop them.

Nilkanth Parmar

•

Why Immich problems stay hidden

Google Photos has whole teams of engineers watching it. Your Immich box has you, checking in when you remember.

Most Immich failures don't take the whole thing down. The web UI loads fine while the machine learning container crashes on an out of memory error for the fifth time today. The server answers requests while the uploads disk creeps toward 100 percent. The database dump runs on schedule every day and writes to the exact drive that would die alongside your photos.

Each of these looks healthy from the outside. Right up until it doesn't.

The theme for everything below: assume nothing will tell you about a problem, and build the signal yourself.

Problem 1: Your database backups live on the same disk as your photos

This one surprises almost everyone. Immich ships with automatic database dumps, switched on by default, running once a day and keeping the 14 most recent. Sounds great. Those dumps get written to UPLOAD_LOCATION/backups, the same folder tree as your library.

That has two consequences. A dead drive takes out your photos and the backup of their metadata in one go. And those dumps don't contain a single photo or video. They hold your albums, faces, descriptions and the map of which file is which. Restore a dump without the files and you get a beautifully organised index pointing at nothing.

Here's what the Immich backup docs say actually needs protecting:

  • The originals. The library, upload and profile folders inside UPLOAD_LOCATION. Lose these and nothing brings them back.

  • Everything else in UPLOAD_LOCATION. Thumbnails, transcoded video and those database dumps. Skip them and Immich can regenerate thumbnails and transcodes after a restore, but on a large library that rebuild takes a long time.

Order matters too. If you can't stop the immich-server container while the backup runs, back up the database first and the files second. Worst case, you end up with a few photos on disk the database doesn't know about, and you upload them again. Flip the order and you can get database rows pointing at files your backup never captured.

Then get a copy off the machine. The old three, two, one rule still holds: three copies, two kinds of storage, one somewhere physically different. Restic or Borg to a remote target, a second NAS at a relative's house, an encrypted bucket. Pick one and schedule it.

And test a restore at least once. The command line restore in the docs expects a fresh install, and its first step tears the stack down along with its volumes using docker compose down -v. Run that against your live server by accident and your backup test becomes the outage. The restore process also changed in v2.5.0, so backups from older versions may need the matching older docs. Better to discover all of this on a spare machine than during a real recovery.

Problem 2: Upgrades that break things you didn't know you depended on

Immich moves fast, and the team is honest about breaking changes. That honesty only helps if you read the notes.

The v3 release earlier this year shows how many separate things one upgrade can touch:

  • pgvecto.rs is gone. If your .env still says DB_VECTOR_EXTENSION=pgvecto.rs, v3 throws an error. You have to migrate to VectorChord first, following the upgrade docs.

  • Several environment variables were removed. The old machine learning preload variables were split into separate textual and visual settings, and the ping timeout moved into the admin UI. Stale values in your compose file stop doing anything.

  • Some older CPUs got left behind. A numpy bump means x86 machines need the v2 microarchitecture level or newer. Very old or very low end chips can fall short.

  • OAuth over plain HTTP is off by default. If your identity provider talks HTTP inside your LAN, logins break until you opt back in from the admin settings.

  • Metric names changed. Underscores became dots, so any Prometheus dashboard or alert built on the old names stops receiving data. Without a single error. An alert that can never fire is worse than no alert, because you think you're covered.

  • API changes broke some outside clients. Endpoints were removed and error responses changed shape, so photo frames and scripts that talk to Immich may need updates.

More is coming. The October project recap says v4 is in planning and will move Immich off Postgres 14. That's a major database version change, exactly the kind of upgrade you want to do on purpose rather than by accident.

How we'd handle every upgrade:

  1. Pin the version. Set IMMICH_VERSION in .env to a specific release instead of release. Your server changes only when you change it.

  2. Keep auto updaters away from Immich. Watchtower style tools are fine for stateless containers. For an app with database migrations, an unattended upgrade means an unattended migration.

  3. Read every release note between your version and the target. Not just the newest one. Breaking changes hide in the middle releases.

  4. Take a fresh database dump and confirm the file exists before pulling. Recovering from a failed migration is far easier with a dump from five minutes ago than one from yesterday.

  5. Upgrade when you can watch it. Not the evening before a trip.

Problem 3: The reverse proxy quietly eats your videos

Photos upload fine. Then someone records a ten minute 4K video and the upload fails at the same point, every single time. That's almost always the proxy, not Immich.

The usual suspects:

  • Body size limits. Nginx defaults to a tiny maximum request size. Immich's own example config sets client_max_body_size to 50000M.

  • Timeouts. Traefik's default timeout is 60 seconds, which the Immich docs say kills uploads after roughly a minute with a 499 error. The official examples raise timeouts to 600 seconds for Nginx, Traefik and Apache.

  • Cloudflare Tunnel on the free plan. Uploads cap out at 100MB per request. Fine for photos, a dealbreaker for long videos. People stuck behind CGNAT run into this constantly, and the common workaround is a small VPS running Nginx, linked back home over Tailscale or WireGuard.

  • Missing WebSocket headers. Live timeline updates need the Upgrade and Connection headers passed through.

  • Sub paths. Immich won't run from something like example.com/photos. Give it its own subdomain.

  • The well known route. If you use the HTTP challenge for Let's Encrypt, route /.well-known/immich to Immich as well, or the mobile app can struggle to connect.

A minimal Nginx block covering the big ones (add the forwarded host and client IP headers from the docs too):

client_max_body_size 50000M;
proxy_request_buffering off;
client_body_buffer_size 1024k;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
proxy_send_timeout 600s;
send_timeout

client_max_body_size 50000M;
proxy_request_buffering off;
client_body_buffer_size 1024k;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
proxy_send_timeout 600s;
send_timeout

client_max_body_size 50000M;
proxy_request_buffering off;
client_body_buffer_size 1024k;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
proxy_send_timeout 600s;
send_timeout

Turning off request buffering matters more than it looks. Without it, Nginx holds the whole upload in memory or temp files before passing it along, which slows big uploads and can choke a small proxy box.


Problem 4: Postgres on a network share

Plenty of homelabs run compute on one box and keep storage on a NAS. That's fine for photos. It's a bad idea for the database.

The Immich requirements are blunt about this: the Postgres data folder belongs on local SSD storage and should never sit on a network share. Postgres depends on file locking and write guarantees that NFS and SMB don't reliably give it, and the failure mode isn't an error today. It's corruption you discover weeks later.

The split that works is simple. Put DB_DATA_LOCATION on a local SSD inside the machine running the containers, and point UPLOAD_LOCATION at whatever big storage you like, NAS mount included. The database is small anyway, usually somewhere around 1 to 3 GB, so a modest SSD covers it with room to spare.

Filesystem matters as well. Use something Unix style that supports proper ownership and permissions, like ext4, ZFS, Btrfs or XFS. An exFAT USB drive formatted on a Windows laptop produces permission errors that look exactly like Immich bugs.

Problem 5: Hardware that looked fine on paper

Immich's documented minimum is 2 CPU cores and 6GB of RAM, with 4 cores and 8GB recommended. People read "minimum" as "comfortable." It isn't, especially during the first import.

Point Immich at 80,000 photos and the machine learning container runs face detection, face recognition and Smart Search embeddings on every one of them, alongside thumbnail generation and video transcoding. On a small box, that's where the out of memory kills start. The container dies, restarts, picks the queue back up, dies again. The web UI keeps working the whole time, which is why people miss it for days.

What helps:

  • Lower job concurrency in the admin job settings during the initial import, then raise it once the backlog clears.

  • Pause machine learning on a 4GB machine. Immich runs without it, just without search and faces, and it catches up once you switch it back on.

  • Run machine learning somewhere else. You can point the server at a remote machine learning endpoint, so a gaming PC does the heavy lifting while a low power box serves the library.

  • Budget disk for generated files. Thumbnails and transcodes add roughly 10 to 20 percent on top of your originals.

Problem 6: The phone stopped backing up and nobody noticed

This is the scenario from the top of the post, and it's the most common way people lose photos while running Immich.

Background uploads depend on the phone letting the app run. Android battery optimization, especially on Samsung, Xiaomi and OnePlus phones, aggressively kills background apps. iOS restricts background work far more than most people expect. The result is a backup that works whenever you open the app and drifts out of date whenever you don't.

Now add Free Up Space, the feature that arrived in v2.5 to delete local copies of photos already on the server. Handy. It also means the phone stops being your second copy. If your server isn't backed up off site, Free Up Space quietly turns one disk into the only home your photos have.

What we'd do:

  • Exclude Immich from battery optimization on every family phone. dontkillmyapp.com has instructions per brand.

  • Check the backup status inside the app on each phone every week or so, until you trust it.

  • Use Free Up Space only after your server has an off site backup you've actually restored from.

Problem 7: Deciding how exposed to be

There are two camps. One keeps Immich on a private network and reaches it over Tailscale or WireGuard. The other puts it on the public internet behind a reverse proxy, so family can open shared links without installing anything.

Private access is the safer default. Nothing listens on the open internet, and your attack surface shrinks to the VPN. The tradeoff is friction: every phone needs the VPN client connected for backups to run, which loops straight back to Problem 6.

Public access makes sharing painless and makes you the security team. Stay on current releases (a v3.3 patch fixed two vulnerabilities in components that ship with Node.js), serve over HTTPS, use OAuth or strong unique passwords, and add Fail2Ban or rate limiting at the proxy. Passkey login is arriving with v3.4 on the web, with mobile support still being worked out.

How to Self Host Immich Without Losing a Single Photo

What breaks

How you find out without monitoring

Fix

Database dumps on the same disk

When the disk dies

Copy UPLOAD_LOCATION/backups and your originals off the machine

Unpinned upgrade

Server won't start after a pull

Pin IMMICH_VERSION, read every release note

Proxy limits

Long videos fail at the same point

Raise body size and timeouts, avoid the 100MB tunnel cap

Postgres on a NAS

Corruption weeks later

Keep DB_DATA_LOCATION on local SSD

Machine learning out of memory

Faces and search lag behind

Lower concurrency or move it to another machine

Phone backup stalled

Missing photos when you need them

Battery exclusions and regular checks

Server or tunnel down

Family messages you asking why it's broken

External uptime monitoring with a loud alert

Monitoring Immich with SIOPS

Every problem above shares one trait: Immich won't shout about it. The last row is the one we can help with directly.

SIOPS is the uptime monitor we build, and the thing that sets it apart is the alarm. When a monitored URL goes down, SIOPS rings a loud alarm on your phone that cuts through silent mode and Do Not Disturb, and it can call you too. A Slack message lands in a pile of other Slack messages. An alarm doesn't get buried.

Setting it up for Immich takes a couple of minutes:

  1. Install SIOPS from the Play Store. The iOS app is in development.

  2. Add a monitor for https://photos.yourdomain.com/api/server/ping rather than your homepage. It's a tiny endpoint built for health checks that returns a 200 when the API is up, and the API is what the mobile apps actually talk to.

  3. Turn on the alarm for that monitor, and add a second channel like Slack, email or a webhook if you want a written record of every incident.

One honest caveat. SIOPS checks from outside your network, so it needs a URL the internet can reach. If your Immich lives only on Tailscale, an external monitor can't see it, and something inside your network has to do the watching. That's a fair price for privacy. For publicly reachable installs, which covers most family setups, an outside check is exactly what you want, because it catches the failures your server can't report about itself: a power cut, a dead tunnel, an ISP outage, a crashed container.

Similar Stories

Similar Stories