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.yamlordocker-compose.ymland.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.
| Requirement | Details |
|---|---|
| Production host | Existing Docker Compose deployment |
| Administrative UI | Supported desktop browser |
| Backup capacity | Database dumps, originals, configuration, and optional generated assets |
| Recovery environment | Clean and isolated from production |
| Recovery objective | Albums, 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
.envlocations - 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.
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:
| Component | Back up? | Why |
|---|---|---|
| PostgreSQL dump | Required | Preserves albums, users, metadata, asset records, and relationships |
| Original photos and videos | Required | These cannot be recreated from the database |
| Compose and environment configuration | Required | Recreates image tags, paths, ports, and service settings |
| Database dump directory | Required | Makes Immich-created dumps available after host loss |
| Thumbnails | Optional | Can usually be regenerated, but omission increases recovery time |
| Encoded videos | Optional | Can usually be regenerated, but video transcoding may take hours or days |
| Profile and other non-derived asset folders | Back up | Small but may contain user-managed or non-regenerable files |
| Redis data | Normally no | Queue, cache, and session state should usually be recreated |
| PostgreSQL live volume | Not a substitute for a dump | A 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:
| Copy | Example location | Purpose |
|---|---|---|
| Production | /srv/immich/library | Live Immich data |
| Local backup | /mnt/backup/immich on a NAS or separate disk | Fast restores |
| Off-site backup | Encrypted object storage or a remote NAS | Site 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.
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.
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:
-apreserves directory structure, timestamps, permissions, and links.-HAXpreserves hard links, access control lists, and extended attributes.--numeric-idskeeps numeric ownership without mapping account names.--info=stats2prints 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.
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.
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_LOCATIONto/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.
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.
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
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
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.
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
| Setting | Starting point | Trade-off |
|---|---|---|
| Automatic database schedule | Daily at 2:00 AM, the documented default | More frequent dumps reduce metadata loss but add storage and I/O |
| Immich dump retention | Latest 14, the documented default | Longer retention uses more local storage |
| Media copy | Daily after the dump completes | Large libraries may need snapshots or incremental backup software |
| Local generations | 30 daily generations | Adjust to capacity and deletion risk |
| Off-site copy | After each successful local generation | WAN transfer may lag on large video libraries |
| Restore drill | Quarterly and before major upgrades | Uses time and temporary storage but exposes failures early |
| Generated assets | Include when recovery speed matters | Exclude when backup capacity matters more |
| Redis | Recreate | Users 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 SHA256SUMSagain. - 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
| Step | Action | Applies To |
|---|---|---|
| 1 | Inventory database, media paths, configuration, and versions | Every deployment |
| 2 | Create scheduled and manual database dumps | Immich web UI |
| 3 | Copy UPLOAD_LOCATION and configuration | Docker host |
| 4 | Retain local and encrypted off-site generations | Backup storage |
| 5 | Restore into a clean, isolated instance | Recovery testing |
| 6 | Validate albums, metadata, originals, and generated files | Every 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.