Quick answer

Request a Google Photos Takeout export and download every archive; preparation can take minutes to days. Run immich-go upload from-google-photos against your Immich server with the photos and JSON sidecars so it can apply dates, locations and albums. Import and ML indexing are separate jobs with hardware-dependent durations. Back up both the database and original files, then test a restore.

Contents

By LK Wood IV · 2026-06-11 · ~14 min read · St. Louis County, MO

Immich is the only self-hosted photo library that’s actually replaced Google Photos for most users in 2026 — face recognition, CLIP semantic search (“photos of the lake”), the timeline view, and a mobile backup app that runs automatically in the background. This guide sets up Immich from scratch and migrates an existing Google Photos library using the Takeout export path.

For the Immich vs PhotoPrism vs Ente comparison, see the self-hosted photo library showdown.

Hardware requirements

ScenarioRAMCPUGPU
Small library (<20K photos), no active ML6GB (Immich’s stated minimum)2 coresNot needed
Typical library (20–100K photos)8–16GB4 coresHelpful but not required
Large library (100K+ photos), fast initial index16–32GB6–8 coresCUDA GPU recommended for indexing speed
Running alongside other servicesAdd 4–8GB buffer

ML indexing and the initial upload have separate queues. Their durations depend on photo/video mix, model, CPU/GPU, storage and concurrent jobs. Measure a representative batch on your server and watch job progress and failures before estimating a large import. GPU support can accelerate eligible ML jobs; it does not set a completion deadline for the whole migration.

Step 1: Deploy Immich with Docker Compose

Create the Immich stack directory:

mkdir -p /opt/stacks/immich && cd /opt/stacks/immich

Download the official Docker Compose files:

wget -O docker-compose.yml \
  https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
wget -O .env \
  https://github.com/immich-app/immich/releases/latest/download/example.env

Edit .env:

nano .env

Set these values:

# Upload path — where your photos will be stored
# Point this at your NAS mount or a large volume
UPLOAD_LOCATION=/mnt/photos/immich

# Database credentials (generate strong passwords)
DB_PASSWORD=your-strong-db-password-here

# Timezone
TZ=America/Chicago

Create the upload directory:

mkdir -p /mnt/photos/immich

Pin the Immich version — the :release tag in the official compose file is a moving target. Pin to a specific version to avoid surprise schema migrations:

# Find the latest stable release
IMMICH_VERSION=$(curl -s https://api.github.com/repos/immich-app/immich/releases/latest | grep '"tag_name"' | cut -d '"' -f 4)
echo "IMMICH_VERSION=$IMMICH_VERSION" >> .env

Start Immich:

docker compose up -d

Immich starts four containers, immich_server, immich_machine_learning, immich_redis (a Valkey image, despite the name) and immich_postgres (PostgreSQL with VectorChord). The separate immich-microservices container that older guides list was folded into the server (official compose checked 2026-08-22). The initial startup takes 2–3 minutes while the database initializes.

Access the web UI at http://your-host-ip:2283. Create the admin account on first launch.

Step 2: Configure storage and external libraries

In Immich Settings → Storage:

The default storage structure stores originals in UPLOAD_LOCATION/{userID}/{year}/{month}/. If you prefer to preserve the original directory layout from your existing photo archive, configure an External Library instead:

Administration → External Libraries → Create Library:

  • Point it at an existing folder (e.g., /mnt/photos/archive) that Immich mounts read-only
  • Immich indexes and searches photos in this location without copying or modifying them
  • Use this path for existing photo archives you want to browse in Immich but manage outside of it

For Google Takeout imports and new phone backups, let Immich manage the storage via the Upload path.

Step 3: Mobile backup setup

Immich mobile app is available on iOS and Android (search “Immich” in the App Store / Play Store).

After installing:

  1. Open the app → enter your server URL: http://192.168.1.10:2283 (or your NPM domain if you’ve set that up)
  2. Log in with your account
  3. Tap the profile icon → Backup → configure backup on WiFi only or on any network
  4. Enable “Background Backup” in the app settings

For background backup to work reliably on iOS:

  • Go to iOS Settings → Immich → allow background app refresh
  • On iOS, the Immich app must be opened periodically to trigger background sync (iOS background task limitation)

For full automatic background backup on Android, enable the “Ignore Battery Optimization” option in the app.

Step 4: Enable hardware ML acceleration

Hardware acceleration dramatically reduces the time to index your library. Configure it based on your hardware.

NVIDIA GPU (CUDA):

In docker-compose.yml, under immich-machine-learning:

  immich-machine-learning:
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}-cuda
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]

The NVIDIA Container Toolkit must be installed on the host:

# On Proxmox or Docker host
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
  sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
  tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
apt update && apt install -y nvidia-container-toolkit
systemctl restart docker

Intel Quick Sync (VAAPI) / OpenVINO:

  immich-machine-learning:
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}-openvino
    devices:
      - /dev/dri:/dev/dri

After changing the image and adding device access, redeploy:

docker compose pull
docker compose up -d

In the Immich web UI → Administration → Jobs → Machine Learning: confirm the GPU is being used (processing speed should increase noticeably).

Step 5: Migrate Google Photos via Takeout

Request your Takeout export:

  1. Go to takeout.google.com
  2. Deselect everything, then select “Google Photos” only
  3. Choose “Export once”, maximum file size (50GB per archive), .zip format
  4. Google emails you when the export is ready. Its Takeout help gives a range of minutes to days, depending on the amount of data.

Download all the zip files to your server. The archive count depends on your library size and the maximum archive size you selected. At up to 50 GB per archive that is a serious one-time download, so if your ISP enforces a monthly cap, check it against your normal usage with the data usage calculator before you kick it off.

Install immich-go, the tool that handles Takeout import correctly (preserving metadata from .json sidecars):

# Get the latest release from GitHub
IMMICH_GO_VERSION=$(curl -s https://api.github.com/repos/simulot/immich-go/releases/latest | grep '"tag_name"' | cut -d '"' -f 4)
# Pick the build for your architecture. Most homelab hosts are x86_64;
# use immich-go_Linux_arm64.tar.gz on a Pi or other ARM box.
ARCH_FILE=immich-go_Linux_x86_64.tar.gz
wget "https://github.com/simulot/immich-go/releases/download/${IMMICH_GO_VERSION}/${ARCH_FILE}"
tar xzf "${ARCH_FILE}"
mv immich-go /usr/local/bin/

Get your Immich API key: Immich web UI → Profile → API Keys → New API Key.

Run the import:

# Extract all Takeout zips to a staging directory
mkdir -p /mnt/staging/takeout
for f in /mnt/staging/takeout-zips/*.zip; do
  unzip -q "$f" -d /mnt/staging/takeout
done

# Import with immich-go
immich-go upload from-google-photos \
  --server=http://localhost:2283 \
  --api-key=YOUR-API-KEY \
  /mnt/staging/takeout
# or skip the unzip step and point it straight at the archives:
# immich-go upload from-google-photos --server=http://localhost:2283 --api-key=YOUR-API-KEY /mnt/staging/takeout-zips/*.zip

The from-google-photos subcommand reads the .json sidecar files and applies their metadata; current immich-go releases replaced the old --google-photos and --create-albums flags with it, and album recreation is on by default through --sync-albums (default true in the upload command reference, checked 2026-08-22).

Watch import progress with immich-go output: it reports files processed, skipped and errors. Upload completion does not mean that Immich’s thumbnail, face-detection and search-indexing jobs have finished; check those queues separately.

Handle duplicates: If you’ve already imported some photos via mobile backup before running Takeout, immich-go detects duplicates by file hash and skips them. You won’t get duplicates in your library.

After import completes, let Immich run ML indexing on the new photos. In the web UI → Administration → Jobs → Face Detection: queue this job to run. It can take hours on a large library.

Step 6: Reverse proxy with Nginx Proxy Manager

Get your Immich accessible at a clean URL instead of 192.168.1.10:2283:

In Nginx Proxy Manager → Add Proxy Host:

  • Domain: photos.yourdomain.com
  • Scheme: http
  • Forward to: immich-server container name (or LAN IP if separate host)
  • Port: 2283
  • SSL: enable with your wildcard cert
  • Client max body size: Set in the Advanced tab: client_max_body_size 0; — required for large file uploads from mobile

Update the mobile app: replace the server URL with https://photos.yourdomain.com.

Step 7: Backup strategy

Immich’s official backup documentation is thorough. The short version:

What to back up:

  1. UPLOAD_LOCATION — all your original photos and videos
  2. PostgreSQL database — ML embeddings, face clusters, album structure, metadata

PostgreSQL backup:

The current manual backup procedure uses pg_dump for the Immich database. Save this script as /usr/local/sbin/immich-db-backup on the Docker host and give it root ownership and mode 0700. Match the container name, database name and username to your Compose configuration. It writes a temporary file in the destination directory, rejects failed or empty dumps, checks gzip integrity, and publishes the final filename only after those checks pass:

#!/bin/bash
set -euo pipefail
umask 077
backup_dir=/mnt/backups/immich
tmp=
trap '[[ -z "$tmp" ]] || rm -f -- "$tmp"' EXIT
trap 'logger -p user.err -t immich-backup "Immich database backup failed"; printf "%s\n" "Immich database backup failed" >&2' ERR
install -d -m 0700 "$backup_dir"
tmp=$(mktemp "$backup_dir/.db-XXXXXX.sql.gz")
docker exec immich_postgres pg_dump --clean --if-exists \
  --dbname=immich --username=postgres | gzip > "$tmp"
gzip --test "$tmp"
# Read the whole stream so pipefail also catches decompression errors.
gzip --decompress --stdout "$tmp" | \
  awk '{ bytes += length($0) + 1 } END { exit(bytes == 0) }'
final="$backup_dir/db-$(date -u +%Y%m%dT%H%M%SZ).sql.gz"
mv -- "$tmp" "$final"
tmp=
printf 'Completed backup: %s\n' "$final"

Run it once and inspect the completed dump before scheduling it. Failures return a nonzero exit code and reach syslog and stderr. Connect your scheduler’s failure reporting to a monitored destination and test a failure alert; a local log entry alone cannot notify you. After configuring mail delivery, save this as /etc/cron.d/immich-backup with root ownership and mode 0644, replacing the address:

MAILTO=your-monitored-address@example.com
0 3 * * * root /usr/local/sbin/immich-db-backup

Use the saved Compose version and copies of the photo directories for a restore drill on a separate host or isolated Compose project. Follow the matching version’s official restore instructions, start with a fresh database, and use psql --single-transaction --set ON_ERROR_STOP=on for a manual import. Check that accounts and albums return, metadata matches, and sample originals and videos open. Record which dump and Immich version you restored and whether the checks passed. Gzip validity alone does not prove database or file recovery.

Photo backup via restic:

If you’re running restic for off-site backups, add UPLOAD_LOCATION and your database dump directory to the backup paths. Photos are the highest-priority backup target — originals are only in one place.

A common mistake: backing up only the Docker volumes (which contain thumbnails and cache) instead of UPLOAD_LOCATION. Back up the actual photo directory.

Common issues

ML indexing not starting: Check that immich-machine-learning container is running: docker compose ps. If it’s restart-looping, check logs: docker compose logs immich-machine-learning. CUDA issues usually show as “CUDA not available” — verify the NVIDIA Container Toolkit is installed and the container has GPU access.

Google Takeout import skipping files: immich-go skips files it has already imported (by hash). Run with --dry-run first to see what would be imported without committing.

Mobile app can’t connect: Verify the server URL includes the port if you’re using the direct IP (http://192.168.1.10:2283). If using NPM, verify the client_max_body_size 0 setting is in the advanced config and that the SSL cert is valid.

Thumbnails not generating: Jobs in Immich run asynchronously. After a large import, the thumbnail and ML jobs queue behind each other. Check Administration → Jobs — each job type shows queued/active/failed counts. Failed jobs usually indicate a storage permission issue on UPLOAD_LOCATION.


Comparing Immich to PhotoPrism and Ente before committing? The self-hosted photo library showdown covers all three. For access from anywhere, Tailscale or WireGuard give your mobile app a direct path to Immich without exposing the server publicly.

Sources

Frequently asked questions

How long does Google Takeout take to process?
Google says preparation can take a few minutes to a few days, depending on the amount of data; many exports are ready the same day. You’ll receive an email when the archives are ready. Request the export ahead of the migration and download every archive before the download links expire.
Does Immich import all Google Photos metadata (location, date, albums)?
Yes, with the right process. Google Takeout exports photos alongside .json sidecar files that contain location, creation date, description, and shared album information. Immich’s Google Takeout importer reads these sidecar files and imports the metadata correctly. If you import raw photos without the sidecar files, you lose the metadata.
How much RAM does Immich need?
Immich’s own requirements page (checked 2026-08-22) says minimum 6GB, recommended 8GB, and that Immich ‘requires at least 6GB of RAM’ during asset upload. The ML model for face recognition and CLIP embeddings consumes 2–3GB during indexing, and the database (PostgreSQL with the VectorChord extension) needs another 1GB at idle. In practice, 8–16GB is comfortable for a library under 100K photos. For initial indexing of a large library, Immich will use all available RAM until the index is complete — plan accordingly.
Can I keep my original photos without Immich modifying them?
Immich stores photos at the path you configure in UPLOAD_LOCATION. The originals are never modified or moved once stored. Immich generates thumbnails and ML embeddings alongside the originals in a separate directory structure. This means you can rsync UPLOAD_LOCATION to any backup target and recover the originals without Immich.
What happens to the Google Photos metadata that doesn’t match a file?
Google Takeout sometimes creates more .json files than photos (for shared albums, duplicates it filtered, etc.). The immich-go tool reports these orphaned sidecar files during import so you can review them. Most are benign — duplicates Google filtered, shared album copies you already have.

Sources and corrections

Last updated
Methodology
See our methodology for research and review standards.
Corrections
Spotted an error or a stale number? Email contact@techfuelhq.com. Confirmed corrections are recorded here with the date and change.

About the author

Written by Lowell K. Wood IV, who builds and runs TechFuelHQ from St. Louis, Missouri.