Immich Photo Backup - Self-Hosted Google Photos with Docker Compose
Status: Active
Last Updated: 2026-08-26
Category: Cloud - Photo Management
Prerequisites: docker-compose-intro, backup-to-object-storage
Time: 2-3 hours
Tags: immich, photos, docker-compose, mobile-backup, postgres
Summary
Deploying Immich โ self-hosted photo and video backup with mobile apps โ on fogserv.cloud: a complete Docker Compose stack, phone backup configuration, how Immich lays out its library on disk, and the two-part backup strategy (Postgres database + uploads directory) that protects it.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ Deploy Immich with its required Postgres + Redis services
- โ Configure automatic camera-roll backup from mobile devices
- โ Understand Immich's upload/library storage layout
- โ Back up both halves of Immich's data consistently
- โ Restore an Immich instance from those backups
Table of Contents
- Architecture Overview
- Docker Compose Deployment
- Mobile App Backup Setup
- Library Storage Layout
- Backing Up Immich
- Restore Walkthrough
Context / Why This Matters
Immich replaces commercial photo clouds: automatic phone upload, timeline, faces, albums, sharing โ entirely self-hosted. Unlike Jellyfin's re-downloadable media, photos here are irreplaceable originals, so Immich gets the strictest treatment in our tiering model (storage-backup-strategies): versioned backups of both its Postgres database (metadata, faces, albums) and its raw upload files. Losing either half alone renders the other half useless.
Implementation / Core Content
Architecture Overview
A production Immich deployment is multi-container:
| Container | Role |
|---|---|
immich-server |
API + web UI; reads/writes uploads |
immich-machine-learning |
Face/object recognition, smart search |
postgres (+vector extension) |
All metadata: assets, users, albums, faces |
redis (Valkey) |
Job queue between server and ML workers |
Critical mental model: files in upload/ are dumb blobs; the database holds everything that makes them a photo library (names, dates, GPS, thumbnails index, album membership). Both must be backed up together from roughly the same point in time.
Docker Compose Deployment
# /opt/immich/docker-compose.yml
name: immich
services:
immich-server:
image: ghcr.io/immich-app/immich-server:v1
restart: unless-stopped
env_file: .env
depends_on:
- redis
- database
volumes:
- ${UPLOAD_LOCATION}/library:/usr/src/app/upload # ALL user data lives here
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:2283:2283" # loopback only; publish via reverse proxy
immich-machine-learning:
image: ghcr.io/immich-app/immich-machine-learning:v1
restart: unless-stopped
env_file: .env
volumes:
- ./model-cache:/cache # re-downloadable ML models
redis:
image: valkey/valkey:8
restart: unless-stopped
database:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3
restart: unless-stopped
env_file: .env
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes:
- ./pgdata:/var/lib/postgresql/data
# /opt/immich/.env
UPLOAD_LOCATION=/srv/immich
DB_PASSWORD=$(openssl rand -base64 24) # generate once, store in vault
DB_USERNAME=immich
DB_DATABASE_NAME=immich
IMMICH_VERSION=release # pin after first deploy!
cd /opt/immich && docker compose up -d
docker compose logs -f immich-server # wait for web ready
First-run wizard at http://server.lan:2283 creates the admin account. Pin IMMICH_VERSION to a concrete tag before going into daily use โ Immich moves fast, and upgrades migrate the DB schema (always snapshot first).
Publishing externally follows the same Traefik label pattern as jellyfin-media-server, routing to port 2283; TLS per ../security/tls-configuration.md. Mobile clients require valid HTTPS for background uploads to be reliable.
Mobile App Backup Setup
On Android/iOS:
- Install the Immich app; set Server URL (
https://photos.fogserv.cloud) and log in - Backup โ Select albums: choose Camera (and Screenshots if wanted)
- Enable Background backup so new photos sync without opening the app
- Optional but recommended for trust-building: enable backup verification badge showing pending vs synced counts
Server-side controls worth setting immediately (Administration โ Settings):
- Storage template: off by default โ see layout section below before enabling
- Job concurrency: leave defaults unless ML jobs starve the box; face recognition is CPU-heavy
- Video transcoding: set to a low target or disable if storage/CPU constrained โ originals are always kept regardless
Library Storage Layout
Under ${UPLOAD_LOCATION}/library, Immich organizes as:
/srv/immich/library/
โโโ <user-uuid>/ # one tree per user
โ โโโ <originals>/ # original files, original filenames preserved
โ โโโ thumbs/ # generated previews (re-generatable)
โ โโโ encoded-video/ # transcode outputs (re-generatable)
โ โโโ profile/
Key implications:
- Originals are sacred: never edit/delete inside
<uuid>/.../originals/; do it through the app. - Thumbs/encoded-video can be regenerated via admin jobs โ exclude them from backups to halve backup size if space-constrained.
- The optional storage template engine renames originals into patterns like
2026/2026-08-26/IMG_1234.jpg. It changes on-disk paths, not content. If you want filesystem browsability, enable it early โ enabling later triggers a large re-shuffle job. Trade-off: external tools (and humans browsing disk) see templated names instead of camera originals.
External libraries (existing folders mounted read-only, e.g., your old photo archive) can be added per-user under External Libraries, with Immich watching rather than owning those files โ useful for migrating without duplicating terabytes.
Backing Up Immich
Two-part nightly job (restic mechanics in backup-to-object-storage):
#!/usr/bin/env bash
set -euo pipefail
source /etc/restic-env # RESTIC_REPOSITORY=s3:http://minio.internal:9000/restic-immich
# Part 1: consistent DB dump FIRST (before file copy)
docker compose -f /opt/immich/docker-compose.yml exec -T database \
pg_dump --username immich immich \
| gzip \
| restic backup --stdin --stdin-filename immich-db.sql.gz --tag immich-db
# Part 2: originals only โ skip regeneratable artifacts
restic backup "${UPLOAD_LOCATION}/library" \
--tag immich-files \
--exclude '/srv/immich/library/*/thumbs/**' \
--exclude '/srv/immich/library/*/encoded-video/**'
# Retention
restic forget --prune \
--keep-daily 7 --keep-weekly 5 --keep-monthly 12 \
--max-repack-size 20G
# Integrity spot-check weekly
restic check --read-data-subset=10%
Why this order: dumping the DB first guarantees the metadata snapshot predates any file churn during part 2; on restore you replay forward, and Immich's asset-hash logic reconciles minor drift. Never back up pgdata/ by direct file copy while running โ Postgres files are inconsistent mid-write.
Restore Walkthrough
# 1. Redeploy stack (compose file from git, fresh volumes)
cd /opt/immich && docker compose up -d database
sleep 15
# 2. Restore DB
gunzip -c <(restic restore latest:/immich-db.sql.gz --to-stdout 2>/dev/null || \
restic dump latest immich-db.sql.gz) \
| docker compose exec -T database psql --username immich --dbname=immich
# 3. Restore files
restic restore latest --target / \
--include '/srv/immich/library/*'
# 4. Bring everything up, regenerate missing thumbs
docker compose up -d
# Admin UI โ Jobs: run "Generate Thumbnails" and "Face Detection" as needed
Full rebuild time is dominated by file restore size; ML re-processing (if thumbs were excluded) adds background hours but no user-visible loss โ originals and metadata come back intact.
Practical Examples
Example 1: Verify phone backups are actually completing
# Server-side count of assets vs what the phone reports
docker compose exec -T database psql -U immich immich -tAc \
"SELECT COUNT(*) FROM assets WHERE 'originalPath' IS NOT NULL;"
Compare against the app's "X items backed up". Divergence usually means background backup was killed by OS battery optimization โ exempt Immich from battery saving on Android.
Example 2: Migrate an existing photo archive without re-upload
Mount old archive read-only, create External Library pointing at it, let Immich hash and index in place. Files stay where they are; Immich tracks them in DB. Subsequent restic coverage then needs both /srv/immich/library and the external mount path added to the backup command.
Example 3: Pinned-version upgrade
cd /opt/immich
source /etc/restic-env && restic backup /opt/immich/.env --tag pre-upgrade
sed -i 's/^IMMICH_VERSION=.*/IMMICH_VERSION=v1.13x.x/' .env
docker compose pull && docker compose up -d
docker compose logs -f | grep -i migrat # watch schema migration finish
Rollback = previous tag + DB restored from the pre-upgrade snapshot (schema downgrades are unsupported).
Troubleshooting & Common Pitfalls
| Problem | Cause | Fix |
|---|---|---|
| Phone stops uploading in background | OS battery optimization kills app | Exempt Immich app from battery saver; check "pending items" badge |
| Face recognition extremely slow | ML container CPU-bound on huge backlog | Raise ML replicas temporarily; run overnight; check job queue in admin UI |
| Restored instance shows empty timeline | DB restored but files missing (or vice versa) | Always restore BOTH parts; verify originals/ tree exists for each user UUID |
| Disk fills with duplicate-looking files | Storage template enabled twice / watch duplicates in app | Check for true hash duplicates (Admin โ Duplicate detection) before deleting anything |
| Upload fails over HTTPS externally | Reverse proxy body-size limit | Raise client_max_body_size / Traefik buffering limits for large videos |
| Backup repo grows every night | Thumbs/encoded-video included | Add the exclude patterns shown above |
| Container crashes on start after upgrade | Skipped versions / unpinned tag pulled breaking change | Pin IMMICH_VERSION; step through releases with DB snapshot each time |
Next Steps / Ops Actions
- Register the Immich backup script in systemd timers with failure alerts: backup-automation
- Do one timed restore drill this month and record achieved RTO
- Decide deliberately on storage templates before family members accumulate history
- Review overall retention policy fit: storage-backup-strategies
Sources & Related Articles
External references consulted:
- https://immich.app/docs/install/docker-compose
- https://immich.app/docs/administration/backup-and-restore
- https://immich.app/docs/features/storage-template
Related knowledge-base articles:
Change Log
2026-08-26
- Initial creation by KB writing session.