How-To

How to Set Up Immich Backup and Recovery with Docker Compose

25 min read

The Back Room Tech is reader-supported. We may earn a commission when you buy through links on our site. Learn more.

An Immich backup needs two parts: PostgreSQL and everything required under UPLOAD_LOCATION. Lose either part and you’ll have photos without albums, or albums pointing to missing files.

This setup follows the 3-2-1 rule. It uses scheduled database dumps, media copies, and an isolated restore test. Copying files is easy. A working restore is what counts.

What Is Immich Backup and Restore?

Immich is a self-hosted photo and video system. PostgreSQL stores users, albums, asset records, metadata, and links between them. Upload storage holds original media, thumbnails, and transcoded videos.

Immich can schedule PostgreSQL dumps, run manual dumps, and restore them through the browser. Those tools cover the database. You still need a filesystem backup for media, deployment files, and database dumps.

Redis usually stays out of the durable backup set. It holds temporary queues, cached data, and sessions. Rebuilding it also keeps stale jobs and expired sessions out of the restored instance.

Prerequisites

Make sure you have:

  • An existing Immich deployment managed with Docker Compose
  • An Immich administrator account
  • Shell access to the Docker host
  • Access to the deployment’s compose.yaml or docker-compose.yml and .env
  • The host path referenced by UPLOAD_LOCATION
  • A second storage device or network share with enough free capacity
  • An off-site destination for the third copy
  • A separate hostname, ports, database volume, and media path for restore testing
  • A maintenance window if you want a backup with no writes between components
  • rsync, tar, and Docker Compose on a Linux host, or an equivalent file-copy tool
  • A password manager or encrypted store for secrets excluded from documentation

The commands use Ubuntu 24.04 LTS syntax and Docker Compose v2. Immich and PostgreSQL versions change often. Record the exact image tags in your deployment.

RequirementDetails
Production hostExisting Docker Compose deployment
Administrative UISupported desktop browser
Backup capacityDatabase dumps, originals, configuration, and optional generated assets
Recovery environmentClean and isolated from production
Recovery objectiveAlbums, metadata, originals, and expected generated assets survive restoration

Important: Don’t point a recovery drill at production database volumes, media paths, hostnames, or ports. Keep the test restore isolated from production.

Step-by-Step Guide

Step 1: Map Every Component That Must Survive

Start in the directory that contains the production Compose file. Replace /opt/immich if you use another path.

cd /opt/immich
docker compose config --services

Expected output looks similar to this, though service names can differ:

database
redis
immich-server
immich-machine-learning

Save the resolved Compose configuration. It shows the paths and settings after Compose reads .env. You’ll need this file if the original host fails.

mkdir -p /opt/immich/recovery-metadata
docker compose config > /opt/immich/recovery-metadata/compose.resolved.yaml
docker compose images > /opt/immich/recovery-metadata/container-images.txt
docker compose ps > /opt/immich/recovery-metadata/container-status.txt

Expected result:

/opt/immich/recovery-metadata/
├── compose.resolved.yaml
├── container-images.txt
└── container-status.txt

Read the upload path without printing every environment variable:

cd /opt/immich
awk -F= '$1 == "UPLOAD_LOCATION" {print $2}' .env

Example output:

/srv/immich/library

If the command prints nothing, inspect the resolved Compose file. Find the storage mount used by immich-server. For /srv/immich/library:/usr/src/app/upload, the path before the colon is the host path.

Also record the PostgreSQL image tag and storage mapping:

cd /opt/immich
docker compose images database
docker compose config > /opt/immich/recovery-metadata/compose.resolved.yaml

Record at least:

  • Immich server image and exact tag
  • PostgreSQL image and exact tag
  • Compose file and .env locations
  • Host path mapped to UPLOAD_LOCATION
  • PostgreSQL volume or bind-mount name
  • Backup destination paths
  • Date, time zone, and backup job identifiers
  • Whether thumbnails and encoded videos are included

Keep compose.resolved.yaml private. It may contain database passwords, API keys, and other secrets from .env.

Terminal showing the resolved Docker Compose services, exact container images, and identified upload storage path with all secrets redacted
Windows desktop with a sanitized Compose configuration open in a text editor and UPLOAD_LOCATION plus its host storage mapping highlighted
macOS desktop with a sanitized Compose configuration open in a text editor and the deployment-specific upload storage mapping visible
Linux desktop editor showing a sanitized Docker Compose excerpt with UPLOAD_LOCATION and its related host path highlighted

Expected result: You can identify the database, originals, configuration, generated assets, and exact versions needed to rebuild the deployment.

Step 2: Identify Essential and Optional Backup Data

A complete Immich recovery set has several parts:

ComponentBack up?Why
PostgreSQL dumpRequiredPreserves albums, users, metadata, asset records, and relationships
Original photos and videosRequiredThese cannot be recreated from the database
Compose and environment configurationRequiredRecreates image tags, paths, ports, and service settings
Database dump directoryRequiredMakes Immich-created dumps available after host loss
ThumbnailsOptionalCan usually be regenerated, but omission increases recovery time
Encoded videosOptionalCan usually be regenerated, but video transcoding may take hours or days
Profile and other non-derived asset foldersBack upSmall but may contain user-managed or non-regenerable files
Redis dataNormally noQueue, cache, and session state should usually be recreated
PostgreSQL live volumeNot a substitute for a dumpA raw copy can be inconsistent or version-dependent

List the top-level directories under your upload path:

sudo find /srv/immich/library -mindepth 1 -maxdepth 1 -type d -print

Example output:

/srv/immich/library/library
/srv/immich/library/upload
/srv/immich/library/profile
/srv/immich/library/thumbs
/srv/immich/library/encoded-video
/srv/immich/library/backups

Directory names vary by Immich release and deployment. Treat this output as an example. Inspect your server, then compare it with the official backup and restore documentation.

Expected result: Your plan includes originals and PostgreSQL. Document each excluded generated folder as a storage-versus-recovery-time trade-off.

Step 3: Design the 3-2-1 Storage Layout

The 3-2-1 rule means:

  • Keep 3 copies of the data: production and two backups.
  • Use 2 storage types or independent storage systems.
  • Keep 1 copy off-site.

A practical layout looks like this:

CopyExample locationPurpose
Production/srv/immich/libraryLive Immich data
Local backup/mnt/backup/immich on a NAS or separate diskFast restores
Off-site backupEncrypted object storage or a remote NASSite and host failure

Database dumps inside UPLOAD_LOCATION are useful, but they share the library’s failure point. One failed disk can remove the media and every local dump.

Create a dated backup directory on a separate local destination:

sudo mkdir -p /mnt/backup/immich/generations
sudo chmod 750 /mnt/backup/immich

Confirm that the destination is a separate mounted filesystem:

findmnt /mnt/backup/immich
df -h /mnt/backup/immich

Expected output should show a separate disk or network filesystem:

TARGET SOURCE FSTYPE OPTIONS
/mnt/backup/immich /dev/sdb1 ext4 rw,relatime

If findmnt reports the production device, you’ve made another directory on the same disk. That may help after an accidental deletion. It won’t help when the disk fails.

Expected result: A production storage failure can’t remove all three copies.

Step 4: Configure Automatic Database Dumps in Immich

Sign in to the Immich web interface as an administrator.

  • Click your profile or administration control.
  • Open Administration.
  • Select Settings.
  • Open the Backup section.
  • Review the database backup schedule.
  • Review the retention count.
  • Save changes if you adjusted either value.

The documented defaults are a daily dump at 2:00 AM and retention of the latest 14 backups. Check your installed release. An upgraded deployment may have different settings.

Administration Settings Backup section showing the schedule and retention controls, with both controls highlighted but no assumed custom values

Automatic dumps preserve PostgreSQL metadata. They don’t copy original media to another disk. They also can’t save a server when its only storage device fails.

Expected result: The Backup page shows an enabled schedule and a retention value you chose on purpose.

Step 5: Create and Verify a Manual Database Dump

Create a manual dump before maintenance and while testing the backup process:

  • Open Administration.
  • Select Job Queues.
  • Click Create job.
  • Select Create Database Dump.
  • Confirm the job.
  • Wait for the job to complete.
Administration Job Queues page with the Create job control highlighted
Create job dialog with Create Database Dump selected and the confirmation control highlighted

Check the backup folder from the Docker host. For the example upload path:

sudo find /srv/immich/library/backups -maxdepth 1 -type f -printf '%TY-%Tm-%Td %TH:%TM %10s %f\n'

Expected output should include a new, non-zero file with the current date and time:

2026-08-13 02:14 4839217 immich-db-backup-…

The filename and format depend on the Immich version. Don’t rename the only copy. Copy it into the dated generation beside the media and manifest.

Expected result: A new non-zero database dump exists, and its timestamp matches the manual job.

Step 6: Capture a Consistent Filesystem Backup

Pause writes while you capture the database and files. Ask users to avoid uploads, edits, and deletions during the backup window. A few quiet minutes can prevent mismatched records and files.

Use this order:

  • Stop or pause new writes.
  • Create the PostgreSQL dump.
  • Wait for the dump job to finish.
  • Copy UPLOAD_LOCATION.
  • Copy deployment configuration and the version inventory.
  • Resume writes.

If you can’t pause writes, take the database dump first. Copy the filesystem as soon as it finishes. Store both timestamps together. This limits the uncertain window, but a restore drill must still prove that the generation works.

Set variables for one generation:

BACKUP_DATE="$(date -u +%Y-%m-%dT%H%M%SZ)"
BACKUP_ROOT="/mnt/backup/immich/generations/${BACKUP_DATE}"
UPLOAD_SOURCE="/srv/immich/library"

sudo mkdir -p "${BACKUP_ROOT}/upload"
sudo mkdir -p "${BACKUP_ROOT}/configuration"

Copy the complete upload location:

sudo rsync -aHAX --numeric-ids --info=stats2 \
  "${UPLOAD_SOURCE}/" \
  "${BACKUP_ROOT}/upload/"

Each flag has a job:

  • -a preserves directory structure, timestamps, permissions, and links.
  • -HAX preserves hard links, access control lists, and extended attributes.
  • --numeric-ids keeps numeric ownership without mapping account names.
  • --info=stats2 prints useful totals after the copy.

Expected output ends with statistics similar to:

Number of files: 48,210
Total file size: 1.42T bytes
Total transferred file size: 3.84G bytes

Copy deployment files separately:

sudo cp -a /opt/immich/compose.yaml "${BACKUP_ROOT}/configuration/"
sudo cp -a /opt/immich/.env "${BACKUP_ROOT}/configuration/"
sudo cp -a /opt/immich/recovery-metadata/. "${BACKUP_ROOT}/configuration/"
sudo chmod -R go-rwx "${BACKUP_ROOT}/configuration"

Use docker-compose.yml if that’s your filename. The .env file may contain credentials. Encrypt the destination and restrict access. A public backup share tends to become an incident report.

Create checksums after the copy:

cd "${BACKUP_ROOT}"
sudo find upload configuration -type f -print0 \
  | sudo sort -z \
  | sudo xargs -0 sha256sum \
  | sudo tee SHA256SUMS >/dev/null

Check that the manifest isn’t empty:

sudo wc -l "${BACKUP_ROOT}/SHA256SUMS"

Example output:

48217 /mnt/backup/immich/generations/2026-08-13T022000Z/SHA256SUMS

Expected result: One dated generation contains the matching database dump, media, configuration, image versions, and checksum manifest.

Step 7: Optionally Exclude Regenerable Assets

Thumbnails and encoded videos can consume much of your backup space. Immich can usually rebuild them from originals and database records. Excluding them makes sense when you can accept a slower recovery.

Check the directory names on your deployment before using exclusions:

sudo rsync -aHAX --numeric-ids --info=stats2 \
  --exclude='/thumbs/' \
  --exclude='/encoded-video/' \
  /srv/immich/library/ \
  "${BACKUP_ROOT}/upload/"

This cuts storage use and off-site transfer time. Recovery gets slower:

  • Thumbnail rebuilds increase CPU and disk use.
  • Video transcoding may take hours on a large library.
  • Hardware acceleration must work before video jobs can finish quickly.
  • The restored interface may lack previews while jobs run.

Never exclude original media, database dumps, or an unknown directory because du says it’s large. Large doesn’t mean disposable.

Expected result: Your runbook lists each excluded generated folder and explains how to rebuild it.

Step 8: Automate Local Retention

Immich retention only controls automatic database dumps. It doesn’t remove external media generations. Those need a separate policy.

Create /usr/local/sbin/immich-copy-backup.sh with this content:

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

BACKUP_DATE="$(date -u +%Y-%m-%dT%H%M%SZ)"
UPLOAD_SOURCE="/srv/immich/library"
CONFIG_SOURCE="/opt/immich"
BACKUP_BASE="/mnt/backup/immich/generations"
BACKUP_ROOT="${BACKUP_BASE}/${BACKUP_DATE}"

mkdir -p "${BACKUP_ROOT}/upload"
mkdir -p "${BACKUP_ROOT}/configuration"

rsync -aHAX --numeric-ids --info=stats2 \
  "${UPLOAD_SOURCE}/" \
  "${BACKUP_ROOT}/upload/"

cp -a "${CONFIG_SOURCE}/compose.yaml" "${BACKUP_ROOT}/configuration/"
cp -a "${CONFIG_SOURCE}/.env" "${BACKUP_ROOT}/configuration/"
cp -a "${CONFIG_SOURCE}/recovery-metadata/." "${BACKUP_ROOT}/configuration/"

chmod -R go-rwx "${BACKUP_ROOT}/configuration"

cd "${BACKUP_ROOT}"
find upload configuration -type f -print0 \
  | sort -z \
  | xargs -0 sha256sum \
  > SHA256SUMS

find "${BACKUP_BASE}" \
  -mindepth 1 \
  -maxdepth 1 \
  -type d \
  -mtime +30 \
  -print

The final command only lists generations older than 30 days. Run it in listing mode first. Check every path before you add deletion behavior.

sudo chmod 750 /usr/local/sbin/immich-copy-backup.sh
sudo /usr/local/sbin/immich-copy-backup.sh

Expected result:

/mnt/backup/immich/generations/2026-06-…

After checking the list, delete through your backup platform or a reviewed cleanup process. Versioned object storage and snapshots are safer than direct shell deletion. Both give you time to recover from a bad rule, operator error, or ransomware.

Schedule the copy with a systemd timer, cron, or NAS scheduler. Run it after the Immich dump has time to finish. With the default 2:00 AM dump, try 3:00 AM first. Measure the actual duration before settling on that time.

Expected result: A dated generation appears automatically. The retention process identifies old generations under a written policy.

Step 9: Send an Independent Copy Off-Site

Use a backup tool with encryption, retries, checks, and retention. Restic, Borg, and storage-provider clients can all work. The right choice depends on the destination.

Each has drawbacks. Restic can create lots of repository metadata. Borg works best over SSH to a compatible host. Provider clients can tie recovery to one vendor. Test a restore before sending 1.5 TB through any of them.

The command depends on the destination. Keep these controls:

  • Encrypt data before or during upload.
  • Keep credentials outside the backup directory.
  • Enable destination-side versioning or immutability where available.
  • Record failed uploads in monitoring.
  • Verify that remote files can be read.
  • Keep several recovery points instead of one current mirror.

A mirror can copy an accidental deletion within minutes. A versioned backup keeps the older generation.

After the off-site job runs, compare generation sizes:

sudo du -sh /mnt/backup/immich/generations/*

Example output:

1.5T /mnt/backup/immich/generations/2026-08-13T022000Z

Expected result: One complete, encrypted generation exists away from the Immich host and local backup device.

Step 10: Use Windows, macOS, or the Web Interface

The server data stays the same regardless of the administrator’s workstation. The browser starts database jobs. OS tools can inspect backup generations without write access.

Windows

Open Immich in a supported browser. Use Administration > Settings > Backup or Administration > Job Queues.

If Windows has a read-only mounted backup share, check a generation with PowerShell:

Get-ChildItem -Path "Z:\immich\generations" -Directory |
    Sort-Object LastWriteTime -Descending |
    Select-Object -First 5 Name, LastWriteTime

Expected output:

Name LastWriteTime
—- ————-
2026-08-13T022000Z 8/13/2026 2:27:18 AM

Use Windows as the admin workstation unless your supported Immich deployment already runs there. Building a second server design for one backup check adds work without improving recovery.

Windows File Explorer showing dated Immich backup generations on a separate mapped backup drive

macOS

Open Immich in a supported browser and use the same administration paths.

If the backup share is mounted under /Volumes/Backup, list recent generations:

find /Volumes/Backup/immich/generations \
  -mindepth 1 \
  -maxdepth 1 \
  -type d \
  -print

Expected output:

/Volumes/Backup/immich/generations/2026-08-13T022000Z

Use the Mac for management and checks unless it’s already your supported Docker host.

macOS Finder showing dated Immich backup generations on a separately mounted backup volume

Web

Immich keeps database backup settings and manual jobs in the signed-in web interface:

  • Administration > Settings > Backup configures schedule and retention.
  • Administration > Job Queues > Create job creates a manual dump.
  • Administration > Maintenance exposes the supported database restore workflow.

The web interface doesn’t copy media from the server. Mobile camera uploads don’t count as server disaster recovery. They cover a different failure.

Expected result: Administrators on all three platforms can inspect generations and reach the same Immich controls.

Step 11: Build a Clean Restore Instance

Create the test deployment on another host or under a separate Compose project. It needs:

  • A unique Compose project name
  • A new PostgreSQL volume
  • A new Redis volume
  • A new and empty upload directory
  • A different HTTP port
  • A test-only hostname
  • No mounts to production paths
  • No production reverse-proxy route
  • Compatible Immich and PostgreSQL image versions

Create the isolated directories:

sudo mkdir -p /srv/immich-restore-test/app
sudo mkdir -p /srv/immich-restore-test/upload
sudo chmod 750 /srv/immich-restore-test

Copy the saved configuration into the test application directory:

sudo cp -a \
  /mnt/backup/immich/generations/2026-08-13T022000Z/configuration/. \
  /srv/immich-restore-test/app/

Edit the test .env and Compose file. At minimum:

  • Change UPLOAD_LOCATION to /srv/immich-restore-test/upload.
  • Use new database storage.
  • Change the published web port.
  • Set a unique Compose project name.
  • Keep the source Immich and PostgreSQL image tags for the initial restore.
  • Use test-only secrets if the restore procedure permits them.
  • Confirm that no production directory appears in a test mount.

Print the resolved test configuration before starting it:

cd /srv/immich-restore-test/app
docker compose -p immich-restore-test config > /tmp/immich-restore-test.resolved.yaml

Review /tmp/immich-restore-test.resolved.yaml line by line. Every media path, database mount, port, and hostname must belong to the test environment.

Warning: Stop if the resolved configuration contains /srv/immich/library, the production database volume, or another production path. Starting the stack could change live data.

Resolved isolated test Compose configuration showing a unique project name, test-only upload path, separate database volume, and non-production port

Expected result: The test configuration has no path, volume, port, or hostname collision with production.

Step 12: Restore the Files and Start PostgreSQL First

Copy the chosen generation into the empty test upload directory:

sudo rsync -aHAX --numeric-ids --info=stats2 \
  /mnt/backup/immich/generations/2026-08-13T022000Z/upload/ \
  /srv/immich-restore-test/upload/

Check the copied files against the saved checksums before starting Immich:

cd /mnt/backup/immich/generations/2026-08-13T022000Z
sudo sha256sum --check SHA256SUMS

Expected output contains OK for every file:

upload/library/…: OK
upload/backups/…: OK
configuration/compose.yaml: OK

Start only the database service. Replace database if your Compose service uses another name:

cd /srv/immich-restore-test/app
docker compose -p immich-restore-test up -d database
docker compose -p immich-restore-test ps database

Initial output may show:

NAME STATUS
immich-restore-test-database Up 5 seconds (health: starting)

Wait and repeat the status command until it reports healthy:

NAME STATUS
immich-restore-test-database Up 35 seconds (healthy)

A running container only proves that PID 1 hasn’t exited. PostgreSQL may still be preparing its data directory, replaying WAL, or waiting for connections.

Now start the remaining services:

docker compose -p immich-restore-test up -d
docker compose -p immich-restore-test ps

Expected result:

NAME STATUS
immich-restore-test-database Up (healthy)
immich-restore-test-redis Up (healthy)
immich-restore-test-immich-server Up

Redis starts with a clean volume on purpose. Old queues and sessions don’t restore library data. They can cause stale work and confusing login problems.

Expected result: The isolated stack starts with clean Redis storage and healthy PostgreSQL.

Step 13: Restore the Database from Maintenance

Open the isolated test URL and check the hostname before clicking. Browser tabs look rather similar after midnight.

  • Sign in to the test instance with an administrator account as required by the supported restore workflow.
  • Open Administration.
  • Select Maintenance.
  • Expand Restore database backup.
  • Locate the matching dump.
  • Check its displayed Immich version and creation date.
  • Select Restore.
  • Wait for the operation to finish.
  • Restart services if the installed release asks you to do so.
Administration Maintenance page on the isolated test instance with Restore database backup expanded
Available database backup list showing the backup version, creation date, and Restore control on the isolated instance

If the dump doesn’t appear, check the restored backup directory against the test UPLOAD_LOCATION mapping. Then confirm that the Immich container can read it.

Avoid improvised low-level imports against production. Use the restore steps for the source release in the Immich backup and restore guide.

Expected result: The restore finishes without readiness or compatibility errors. The test instance starts with the recovered library.

Step 14: Validate the Recovery Page and the Login Page

A login page proves that the web server answers HTTP requests. It says little about originals, albums, metadata, or generated files.

Validate albums and asset counts

Open Albums and compare:

  • Representative album names
  • Asset counts
  • Shared album membership
  • Album covers
  • Date ordering
Albums page in the isolated restored instance showing a privacy-safe album name, asset count, and representative synthetic assets

Validate original files

Open a sample that includes:

  • JPEG or HEIC photos
  • RAW files, if used
  • Short and long videos
  • Older and newly uploaded assets
  • Assets from several users or external libraries, if applicable

Download one test original and compare its checksum with the backup copy:

sha256sum /path/to/restored-test-original.jpg
sha256sum /path/to/backup-copy-of-original.jpg

Expected result:

8f… /path/to/restored-test-original.jpg
8f… /path/to/backup-copy-of-original.jpg

The hashes must match exactly. Similar dimensions, filenames, or EXIF data don’t prove byte-for-byte recovery.

Validate metadata

For representative assets, check:

  • Capture date and time
  • Description
  • Favorites
  • Archive state
  • Location metadata, if intentionally retained
  • People or face assignments, if applicable
  • Stack membership
  • Album membership
Privacy-safe restored asset details showing that the original opens and representative metadata is present

Validate generated assets

If the backup includes thumbnails and encoded videos, confirm that previews load and videos play.

If you excluded them, start the correct regeneration jobs for your installed release. Watch queue progress, CPU use, storage, and any configured accelerator. A job stuck at 0% for six hours needs investigation.

Isolated test instance with its test-only hostname visible and restored albums plus assets displayed using synthetic media

Validate counts and logs

Check container health and recent errors:

cd /srv/immich-restore-test/app
docker compose -p immich-restore-test ps
docker compose -p immich-restore-test logs --since 15m immich-server

Expected result: services stay running, originals open, and logs have no repeated file, permission, migration, or database errors.

Expected result: Albums, metadata, originals, and expected generated assets work. The application shell alone doesn’t pass the drill.

Step 15: Handle Backups Created Before Immich v2.5.0 Conservatively

Don’t assume that a pre-v2.5.0 backup can import into the current release. Compatibility depends on the source Immich release, PostgreSQL image, dump format, and upgrade path.

For a legacy backup:

  • Keep the original backup read-only.
  • Record its creation date and any known source versions.
  • Check the Immich releases and version-specific documentation.
  • Recreate an isolated environment using the matching Immich and PostgreSQL versions where possible.
  • Restore the database and media there first.
  • Validate the library at that version.
  • Make a fresh backup from the working restored environment.
  • Follow the supported upgrade path.
  • Never trial the legacy restore against production.

This takes longer, but it protects the original recovery copy and limits version guesswork. Slow and repeatable beats fast and irreversible here.

Expected result: Legacy data works in a matching isolated environment before any upgrade or production recovery decision.

Step 16: Write the Operational Recovery Checklist

Store this checklist with the backup documents and somewhere available while the Immich host is offline. Keeping the only copy on that host would be efficient sabotage.

  • Declare the incident and stop new uploads or edits
  • Preserve the failed host and storage for investigation
  • Select a dated database-plus-media generation
  • Verify its checksum manifest
  • Confirm the recorded Immich and PostgreSQL versions
  • Provision a clean host or isolated Compose project
  • Create new database, Redis, and upload storage
  • Confirm no mount points reference production
  • Restore the upload tree and configuration
  • Start PostgreSQL and wait for healthy readiness
  • Start a fresh Redis instance
  • Start Immich with the source-compatible image versions
  • Restore the listed database backup
  • Validate album names and counts
  • Open and checksum representative originals
  • Validate metadata, dates, favorites, and relationships
  • Validate or regenerate thumbnails and encoded videos
  • Review logs for missing paths, permission errors, and migrations
  • Record recovery time and any manual corrections
  • Take a new backup after recovery
  • Upgrade only after the restored source version is stable

Configuration

Recommended Backup Policy

SettingStarting pointTrade-off
Automatic database scheduleDaily at 2:00 AM, the documented defaultMore frequent dumps reduce metadata loss but add storage and I/O
Immich dump retentionLatest 14, the documented defaultLonger retention uses more local storage
Media copyDaily after the dump completesLarge libraries may need snapshots or incremental backup software
Local generations30 daily generationsAdjust to capacity and deletion risk
Off-site copyAfter each successful local generationWAN transfer may lag on large video libraries
Restore drillQuarterly and before major upgradesUses time and temporary storage but exposes failures early
Generated assetsInclude when recovery speed mattersExclude when backup capacity matters more
RedisRecreateUsers may need to sign in again; queued work is not preserved

These are starting values, not fixed targets. A 200 GB library on NVMe has different copy times and costs than a 12 TB library on spinning disks.

Version Manifest

Keep a plain-text manifest in every generation:

Backup timestamp: 2026-08-13T02:20:00Z
Immich server image: exact-image-and-tag-from-compose
PostgreSQL image: exact-image-and-tag-from-compose
Compose project: production-project-name
Upload source: /srv/immich/library
Database dump: exact-filename-from-backups-directory
Generated folders included: yes/no
Generated folders excluded: list-or-none
Backup host: backup-hostname
Restore drill date: pending

Record exact image digests as well as tags when your deployment pins them. Tags can move or disappear. Digests identify the exact image content you tested.

Recovery Point and Recovery Time

Your recovery point objective, or RPO, sets how much recent data you can lose. With one daily database dump, the worst case is close to 24 hours of metadata changes.

Your recovery time objective, or RTO, sets how long a restore can take. Excluding thumbnails and encoded videos cuts backup size. Immich must rebuild them before the recovered library feels complete.

A balanced homelab policy may keep daily recovery generations and take database dumps more often. It may include generated assets only in less frequent local snapshots. Measure restore time on your hardware. A guessed four-hour RTO can become 36 hours when video transcoding starts.

Tips and Troubleshooting

The dump exists, but the photos do not

Why it happens: PostgreSQL stores records and metadata. It doesn’t store the original photo bytes.

Fix:

  • Find the host path mapped to UPLOAD_LOCATION.
  • Confirm that original photos and videos exist there.
  • Add the entire required media tree to the external backup.
  • Keep the matching database dump in the same dated generation.
  • Repeat the clean restore test.

A database-only backup is incomplete. It can rebuild the catalog, but the catalog can’t recreate a missing JPEG.

Media files exist, but albums and metadata are missing

Why it happens: The media copy lacks its matching PostgreSQL dump.

Fix:

  • Locate a database dump from the same backup window.
  • Restore into a clean, version-compatible instance.
  • Restore the media tree to the expected upload mapping.
  • Import the database using the supported Maintenance workflow.
  • Validate album and metadata relationships.

A media-only copy is incomplete too. The originals survive, but Immich’s organization and user state don’t.

PostgreSQL restore fails while the container is starting

Typical symptom: The restore reports a connection failure, reset, refusal, or unavailable database.

Why it happens: PostgreSQL is still starting, or the test instance uses an incompatible image version.

Fix:

cd /srv/immich-restore-test/app
docker compose -p immich-restore-test ps database
docker compose -p immich-restore-test logs --since 10m database

Wait for a healthy status. Compare the test database image with container-images.txt from the backup. Retry after PostgreSQL is ready.

Assets appear but cannot be opened

Why it happens: The container has the wrong mount, the files sit in an extra directory, or permissions block access.

Fix:

sudo find /srv/immich-restore-test/upload -maxdepth 2 -type d -print
sudo stat /srv/immich-restore-test/upload
docker compose -p immich-restore-test config > /tmp/restore-paths.yaml

Compare /tmp/restore-paths.yaml with the saved production mapping. Then inspect a missing file:

sudo stat /srv/immich-restore-test/upload/path/to/missing-file.jpg

Don’t apply ownership 999:999 from a random online example. Check the user ID used by your containers, then grant it the required access. A recursive chown is quick until it breaks another service.

The restored library behaves incorrectly on a newer Immich release

Why it happens: The backup went straight into an incompatible application or database version.

Fix:

  • Stop the test instance.
  • Keep the backup unchanged.
  • Restore first with the recorded source Immich and PostgreSQL versions.
  • Validate the library at that version.
  • Follow the supported upgrade path.
  • Consult the official repository and release notes.

The database and media represent different times

Why it happens: Uploads, edits, or deletions continued between the dump and filesystem copy.

Fix:

  • Repeat the backup during a no-write window.
  • Create the database dump first.
  • Wait for it to complete.
  • Copy the filesystem immediately afterward.
  • Keep the timestamps and manifest together.
  • Prove consistency with another restore drill.

Filesystem snapshots can shorten the no-write window, but PostgreSQL still needs coordination. A snapshot taken at a random instant can preserve mismatched data.

Automatic dumps exist only on the production disk

Why it happens: Immich made the dump, but no external job copied it.

Fix:

  • Include the backup directory in the local media copy.
  • Monitor the local backup job.
  • Replicate the generation off-site.
  • Enable versioning or immutable retention.
  • Test-read a remote generation.

A local dump covers some operator errors. It doesn’t cover loss of the host, controller, filesystem, or shared disk.

The backup is too large

Why it happens: Every generation contains all thumbnails and transcoded videos.

Fix: Consider incremental backup software, snapshots, deduplication, or planned exclusion of thumbs and encoded-video. Keep originals, PostgreSQL dumps, profile assets, and configuration. Measure rebuild time during the next drill before keeping the exclusion.

Real measurements settle this choice. Saving 600 GB per generation may work well, but a two-day video rebuild may break your RTO.

Redis restore produces stale jobs or broken sessions

Why it happens: Redis contains temporary state tied to the old runtime.

Fix: Start with a clean Redis volume. Users may need to sign in again. You may also need to restart interrupted jobs. PostgreSQL and media remain the main recovery set.

The database dump does not appear under Maintenance

Why it happens: The file is outside the expected backup folder, the mount is wrong, or Immich can’t read it.

Fix:

  • Confirm the dump exists under the restored upload tree.
  • Check the resolved test volume mapping.
  • Check file ownership and permissions.
  • Confirm the dump belongs to the expected Immich version.
  • Restart the test service if the applicable documentation requires a rescan.

Checksums fail after an off-site restore

Why it happens: The transfer is incomplete, a file changed during backup, or two generations were mixed.

Fix:

  • Stop the restore.
  • Preserve the failed copy for diagnosis.
  • Compare the generation identifier and manifest.
  • Re-download the failed files from another retained copy.
  • Run sha256sum --check SHA256SUMS again.
  • Do not accept a partially verified generation for production recovery.

One failed checksum rejects the generation until you find the cause. Selective optimism makes a poor integrity check.

Wrapping Up

StepActionApplies To
1Inventory database, media paths, configuration, and versionsEvery deployment
2Create scheduled and manual database dumpsImmich web UI
3Copy UPLOAD_LOCATION and configurationDocker host
4Retain local and encrypted off-site generationsBackup storage
5Restore into a clean, isolated instanceRecovery testing
6Validate albums, metadata, originals, and generated filesEvery restore drill

A sound Immich backup includes PostgreSQL dumps, original media, saved configuration, separate storage, and recorded image versions. Initial setup takes about 20–30 minutes, plus the first media copy.

Run a clean restore every quarter and before major upgrades. Trust the backup only after albums load, sample SHA-256 hashes match, and logs stay clean.