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:


Table of Contents

  1. Architecture Overview
  2. Docker Compose Deployment
  3. Mobile App Backup Setup
  4. Library Storage Layout
  5. Backing Up Immich
  6. 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:

  1. Install the Immich app; set Server URL (https://photos.fogserv.cloud) and log in
  2. Backup โ†’ Select albums: choose Camera (and Screenshots if wanted)
  3. Enable Background backup so new photos sync without opening the app
  4. Optional but recommended for trust-building: enable backup verification badge showing pending vs synced counts

Server-side controls worth setting immediately (Administration โ†’ Settings):

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:

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

Sources & Related Articles

External references consulted:

Related knowledge-base articles:

Change Log

2026-08-26

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse