Skip to content
Go back

Immich Backups That Outlive Immich

By KingPin 14 min read
Immich Backups That Outlive Immich
Contents

You run Immich for the family. It works, everyone loves it, and then life happens. You get a new job, the upgrade notes pile up, an update breaks the stack, and one day the server sits in a corner untouched for six months. Then your spouse needs the photos from the kid’s first birthday, and you are on a plane.

Back up Immich so the photos are useful without Immich running. That means originals in a readable folder layout, the metadata that lives only in the database exported to files, a normal database dump for the day you do want Immich back, an offsite copy, and a one-page README the family can follow. Photos last decades. Your interest in maintaining a Postgres container might last two years.

Full example: Clone the working files at github.com/KingPin/sumguy-examples/self-hosting/immich-backups-that-outlive-immich

What lives in files and what lives only in Postgres

Start with a precise map, because most “I have a backup” confidence comes from not asking this question.

The Immich docs say it directly: Immich stores file paths and user metadata in the database, and it does not scan the library folder. The originals are on disk and are never modified. Everything Immich knows about them lives in Postgres.

Here is what that means in practice, as of Immich 3.3 (October 2026):

That third bullet is the problem. Lose the database and you still have every photo, in a folder tree, with no album names and no idea who anyone is. Two jobs follow from this. Put the originals in a layout a human can navigate, and get the Postgres-only metadata out into files.

Make the folder layout readable

By default on a new install, Immich keeps your uploads under upload/<user-id>/ in a layout that is meaningless without the database. The storage template fixes that. Turn it on and Immich moves originals into library/<storage-label>/ using a pattern you choose. The admin user’s default storage label is admin.

The default pattern is {{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}, which gives you 2026/2026-08-14/IMG_0412.jpg. I would keep it. It sorts chronologically in any file browser, and if two files collide, Immich appends a sequence number instead of overwriting. Date variables render in the server’s local timezone, so a photo shot at 11:50 PM can land in the next day’s folder if the server’s clock disagrees with where you were.

Set it in the web UI under Administration > Settings > Storage Template, or in a config file. Here is the config-file version, together with the database dump settings from the next section:

immich-config.json
{
"backup": {
"database": {
"cronExpression": "0 02 * * *",
"enabled": true,
"keepLastAmount": 14
}
},
"storageTemplate": {
"enabled": true,
"hashVerificationEnabled": true,
"template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}"
}
}

Point the IMMICH_CONFIG_FILE environment variable at that file. One gotcha: when you set it, the web UI stops letting you edit the other settings it covers. If you want to click around, use the UI and skip the file.

After you enable the template or change it, run the Storage Template Migration job from Administration > Job Queues so existing files move to match. If you turn the template off later, Immich leaves files where they are. Nothing is locked away in an internal format.

If you would rather browse by album on disk, the docs show a conditional template:

{{y}}/{{#if album}}{{album}}{{else}}Other{{/if}}/{{MM}}/{{filename}}

A photo in several albums takes the name of the most recently created one. Special characters in album names become HTML entities unless you wrap the variable in three braces. I find the date layout more stable, because album names change and photos do not. The next section covers albums another way.

Sidecars: the part that is smaller than you think

Immich can write .xmp sidecar files, so metadata survives outside Postgres. digiKam, Darktable, and Lightroom read XMP too. But the field list is short.

Per the XMP sidecar docs, Immich writes only these fields:

MetadataXMP field Immich writes
Descriptiondc:description, tiff:ImageDescription
Ratingxmp:Rating
Date and timeexif:DateTimeOriginal, photoshop:DateCreated
Locationexif:GPSLatitude, exif:GPSLongitude
TagsdigiKam:TagsList

Albums, people, and favorites are not on the list. And the write is event-driven: Immich queues a Sidecar Write job when you edit a description, rating, date, location, or tags. There is no bulk job that writes sidecars for your whole library. Photos you never touched have no sidecar, and their metadata is whatever the camera embedded. That is fine, because the camera data is the part that matters most.

Sidecars use the name IMG_0001.jpg.xmp next to IMG_0001.jpg. When the storage template moves a file, the sidecar moves with it, so the pair stays together. One more danger: if you use an external library mounted read-only, the docs warn that metadata edits fail silently, with no sidecar and no database update. Mount it read-write if you want edits to stick.

So sidecars cover your hand edits to dates, places, descriptions, and tags. They do not cover the things your family will actually ask for, which are albums and people.

Export albums and people as plain folders

Two tools can get album data out, and you can use both.

A script for album and person folders

The script export_metadata.py asks the Immich API which files belong to each album and each named person, then builds folders of relative symlinks. It uses only the Python standard library and an API key from the environment. The key needs album.read, person.read, and asset.read permissions.

In Immich 3.2 the search endpoint added a filter object and a cursor for paging, and deprecated the old albumIds, personIds, and page fields. The script uses the new form. The album response no longer lists its assets, so the script searches by album instead:

export_metadata.py (excerpt)
def asset_paths(filter_):
"""Yield originalPath for every asset matching a v3 search filter."""
cursor = None
while True:
body = {"filter": filter_, "size": 1000}
if cursor:
body["cursor"] = cursor
page = api("POST", "/search/metadata", body)["assets"]
for item in page["items"]:
yield item["originalPath"]
cursor = page.get("nextCursor")
if not cursor:
return

Run it like this:

Terminal window
export IMMICH_URL=http://localhost:2283
export IMMICH_API_KEY=your-key
python3 export_metadata.py --out /srv/immich/export

You get export/albums/Summer 2024/IMG_0412.jpg and export/people/Grandma/IMG_0033.jpg, each a relative symlink into library/. You also get albums.json and people.json, which map every name to its file paths for tools that do not follow symlinks. Keep the output inside your upload location so the relative links survive a restore. The script clears and rebuilds the link folders on every run, so renamed albums do not leave ghosts.

Only named people are exported. Unnamed face clusters have no useful label. Photos in the Locked Folder are left out too, because the search API hides them. I tested the script against a mock API that returns the documented response shapes, not against a live server, so run it once by hand and check the folders before you schedule it.

immich-go for a second archive

immich-go v0.32.0 has an archive command that exports from an Immich server to a local folder:

Terminal window
immich-go archive from-immich \
--from-server=http://localhost:2283 \
--from-api-key=your-key \
--write-to-folder=/srv/photo-archive

It writes a YYYY/YYYY-MM/ tree with a .JSON file beside each photo. That file records the GPS position, date taken, description, albums, tags, rating, and favorite, archive, and trash flags. The metadata structure has no field for people, so my script still earns its place. Note that this archive is a second full copy of your originals, so it doubles the storage. I would use it as a periodic escape hatch before a risky upgrade, not as the daily backup. The destination folder is not wiped first, so you can rerun it to add new photos.

Database backups for the “I still want Immich” case

Immich creates database dumps for you. The defaults are daily at 2:00 AM, keeping the last 14, written to UPLOAD_LOCATION/backups as .sql.gz files. You adjust them under Administration > Settings > Database Dump Settings, or in the backup.database block of the config above. To take one on demand, go to Administration > Job Queues, click Create job, and choose Create Database Dump.

Two things to know. First, the dumps contain no photos, only metadata, so they work only next to a copy of the files. Second, the backup and restore process changed in v2.5.0. Dumps from older versions need the manual instructions from the matching docs version.

Restoring is a menu action, not a ritual: Administration > Maintenance > Restore database backup. On a fresh install, you copy your old backups, library, profile, thumbs, upload, and encoded-video folders into the new upload location, start the stack, and click Restore from backup on the welcome screen. Immich takes a restore point first and rolls back if the health check fails.

The order matters when you copy files out. The docs recommend stopping immich-server during a backup. If you cannot, back up the database first and the files second. The worst case is then files the database does not know about, which you can re-upload. The reverse order can leave a database pointing at files your backup never saw.

The offsite copy

The local copy protects you from a bad upgrade. It does not protect you from a dead disk, a fire, or a theft. Use restic to push to object storage. It encrypts, deduplicates, and keeps snapshots, so a corrupted file today does not overwrite yesterday’s good one.

offsite-backup.sh
#!/usr/bin/env bash
# Offsite backup of an Immich install with restic.
# Run daily from cron or a systemd timer, after Immich's own 02:00 database dump.
set -euo pipefail
UPLOAD_LOCATION="${UPLOAD_LOCATION:-/srv/immich}"
: "${RESTIC_REPOSITORY:?set RESTIC_REPOSITORY, e.g. s3:s3.example.com/family-photos}"
: "${RESTIC_PASSWORD_FILE:?set RESTIC_PASSWORD_FILE}"
export RESTIC_REPOSITORY RESTIC_PASSWORD_FILE
# 1. Export album and person membership as plain symlink folders.
python3 "$(dirname "$0")/export_metadata.py" --out "$UPLOAD_LOCATION/export" --prefix-to "$UPLOAD_LOCATION"
# 2. Originals, the database dumps, and the export. Skip thumbs and encoded-video
# (Immich regenerates both).
cd "$UPLOAD_LOCATION"
restic backup --tag immich library upload profile backups export
# 3. Keep 7 daily, 8 weekly, 12 monthly, 5 yearly snapshots, then verify.
restic forget --tag immich --keep-daily 7 --keep-weekly 8 --keep-monthly 12 --keep-yearly 5 --prune
restic check --read-data-subset=5%

The script runs the album export first, backs up the folders that matter, then prunes and spot-checks. Schedule it at 3:30 AM so it runs after the 2:00 AM database dump finishes. That gives you the safe ordering from the docs: database first, files second.

Initialize the repository once with restic init, and put the password file somewhere other than the server it protects. A password you lose means an offsite copy you cannot open. The first run is slow, since it uploads everything. After that, restic sends only changed data. For a comparison of restic and rclone, see the posts linked at the end.

If you would rather have a plain, browsable mirror next to the encrypted repository, rclone copy /srv/immich/library remote:photos-plain does it without deleting anything at the destination. A plain mirror is easier for a family member to open. An encrypted repository is safer if the bucket leaks. Keeping both doubles the storage bill.

The README for the people who are not you

A backup that only you can restore is a single point of failure with a pulse. Write a one-page note and put it where the family already looks: the shared drive, the password manager, a printed copy in the file cabinet. The example repo has a template, FAMILY-README.md. Its core is a short ladder:

  1. Open the library folder in any file browser. Every photo is a normal file, sorted by year and day.
  2. Open export/albums or export/people to browse by album or by person.
  3. If the server is gone, install restic, set the repository and password, and run restic restore latest --target ./family-photos. Restic restores the full original path, so the photos land under family-photos/srv/immich/library.
  4. If they want a photo app again, hand the newest .sql.gz file to a technical friend and point at the Immich restore docs.

Add the repository location, the name of the password manager entry (never the password), and a phone number for whoever can help. digiKam is a good free viewer on Windows, macOS, and Linux, since it reads dates, locations, and tags from both the files and the .xmp sidecars.

The yearly test

Backups fail quietly, and READMEs go stale. Once a year, on a day you will remember, run this drill:

  1. Pick one random year of photos and restore it from the offsite repository into an empty folder: restic restore latest --target /tmp/drill --include /srv/immich/library/admin/2019. Restic stores absolute paths, so adjust the path to match your layout.
  2. Open the folder in a plain file browser. Do the photos open? Are they the right ones?
  3. Open export/albums in the restored copy. Do the links resolve?
  4. Hand the README to the least technical family member and ask them to find one photo. Do not help.
  5. Spot-check the newest .sql.gz in backups: gunzip -t should exit silently.
  6. Fix whatever broke, and update the README with the new date.

The fourth step is the one that matters. If your spouse cannot get a photo with only the README, the backup does not do its job yet.

The SumGuy Take

Immich is a fine app and you should keep running it. But treat it as a viewer on top of your files, not as the vault. The vault is a folder tree of originals, a handful of exported metadata files, a rotating database dump, and an encrypted copy somewhere else. If Immich stops tomorrow, none of that changes, and your family never has to learn what Postgres is.

Common Questions

Does Immich modify my original photo files?

No. Immich keeps originals unmodified and stores your edits in its database. The editor’s crop, rotate, and mirror changes create a new version while the original stays intact. The only files it writes next to originals are .xmp sidecars, and those hold a short list of fields.

Can I back up only the library folder?

No. The Immich docs say only library, upload, and profile contain content you cannot regenerate. Back those up too, plus the backups folder, which holds the database dumps. You can skip thumbs and encoded-video and rerun the thumbnail and transcoding jobs after a restore.

Do I have to stop Immich while I back it up?

No, but the order matters. If you keep the server running, back up the database first and the files second. The worst case is then files the database does not know about, which you can re-upload. Backing up files first can leave the restored database pointing at missing files.

Can I restore a database backup from an older Immich version?

Often, but the Immich docs give no guarantee. The restore screen shows whether each backup matches your current version and attempts any needed database migrations. Backups made before v2.5.0 need the manual process from the docs version matching the backup. Restoring into the same Immich version that made the backup is safest.


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.


Next Post
Claude Skills vs MCP: What Each One Is For

Discussion

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

Related Posts