← All guides

Set Up Immich on Your Mac: Self-Hosted Google Photos for Your Family

Replace Google Photos with Immich running on your Mac. Docker Compose setup, automatic iPhone backup, face recognition, ML tuning for Apple Silicon, and backup strategy.

What you’ll build: A self-hosted photo library running on your Mac, with automatic backup from iOS and Android.

End state: Immich running as four Docker containers, accessible from your phone and any browser on your network. Photos sync automatically when you’re home, face recognition and semantic search work in the background.

What you’ll understand: How the four containers fit together, how to keep the ML service from saturating your CPU, and how to back up the database so a bad update doesn’t cost you years of photo metadata.

If you’ve been looking for a self-hosted Google Photos alternative that doesn’t feel like a science project, Immich is it. Automatic backup from iOS and Android, face recognition, semantic search, shared albums, and a web UI that your family can actually use without calling you. If you’ve been sitting on a Google Takeout export wondering where to put it, this is where those photos land.

Immich mobile app showing the photo timeline

Prerequisites: OrbStack installed, a home Docker network created, and your data directories chosen. The Mac preparation guide covers all of that.

What you’re deploying

Immich runs as four containers:

ServiceWhat it does
immich-serverThe main application: API, web UI, background job scheduler
immich-machine-learningFace detection, CLIP embeddings for semantic search
redis (Valkey)Job queue and caching
postgresDatabase with vector search extensions

The Postgres image is not the standard one. Immich ships their own build with vectorchord and pgvectors extensions baked in. Those power face clustering and the “search for photos of your daughter at the beach” kind of queries. Don’t substitute a plain Postgres image. It won’t work.

Directory layout

Create the directories before starting the stack. Immich doesn’t create them itself, and if they’re missing, Docker creates them as root:root and everything breaks in confusing ways.

mkdir -p ~/server/data/immich/library
mkdir -p ~/server/data/immich/postgres
mkdir -p ~/server/data/immich/db-dumps

No output means success. Verify with ls ~/server/data/immich/ if you want to be sure:

drwxr-xr-x  library
drwxr-xr-x  postgres
drwxr-xr-x  db-dumps

The postgres directory should ideally live on an SSD. The internal NVMe or a fast external SSD both work. The Immich docs explicitly say never to use a network share for the database. Whether a spinning HDD is acceptable depends on your library size and tolerance for sluggishness; the official guidance is SSD. I keep mine on the internal NVMe under ~/server/data/.

Plan for more space than your originals.

Real-world storage: our family library

Count / Size
Photos~14,600
Videos~1,400
Originals on disk150 GB
Transcoded video52 GB
Thumbnails8 GB
Database496 MB
Total on disk211 GB

That’s about 40% overhead on top of the originals, mostly from transcoded video. If you shoot a lot of video on your phone, budget accordingly.

Docker Compose

Create ~/server/stacks/immich/docker-compose.yml:

name: immich

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    networks:
      - home
    volumes:
      - ${UPLOAD_LOCATION}:/data
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    ports:
      - "2283:2283"
    depends_on:
      - redis
      - database
    restart: always
    healthcheck:
      disable: false

  immich-machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    # Runs on CPU. Apple Silicon doesn't support GPU acceleration inside Linux containers.
    networks:
      - home
    volumes:
      - model-cache:/cache
    env_file:
      - .env
    restart: always
    healthcheck:
      disable: false

  redis:
    container_name: immich_redis
    image: docker.io/valkey/valkey:8-alpine
    networks:
      - home
    healthcheck:
      test: redis-cli ping || exit 1
    restart: always

  database:
    container_name: immich_postgres
    image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
    networks:
      - home
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_DB: ${DB_DATABASE_NAME}
      POSTGRES_INITDB_ARGS: '--data-checksums'
    volumes:
      - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
    restart: always
    healthcheck:
      disable: false

volumes:
  model-cache:

networks:
  home:
    external: true

A few config lines to understand:

Environment configuration

Create ~/server/stacks/immich/.env:

# Where uploaded photos and videos are stored
UPLOAD_LOCATION=/Users/yourname/server/data/immich/library

# Where Postgres stores its data — must be on SSD
DB_DATA_LOCATION=/Users/yourname/server/data/immich/postgres

# Your timezone
# Full list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
TZ=Europe/Berlin

# Immich version — 'release' tracks latest, or pin to a specific version
IMMICH_VERSION=release

# Database credentials — use something random for DB_PASSWORD
# Alphanumeric only, no special characters
DB_PASSWORD=changeme_use_something_random
DB_USERNAME=postgres
DB_DATABASE_NAME=immich

Generate a random password and write it directly into the .env file:

DB_PASS=$(openssl rand -hex 12) && \
  sed -i '' "s/changeme_use_something_random/${DB_PASS}/" .env && \
  echo "Password set: ${DB_PASS}"

You should see something like:

Password set: a3f8c2d91e4b7f06c2a8

The sed -i '' syntax is macOS-specific.

Start the stack

From the stacks/immich/ directory:

docker compose up -d

You should see Docker pulling images on first run, then starting the containers:

[+] Running 4/4
 ✔ Container immich_redis                Started
 ✔ Container immich_postgres             Started
 ✔ Container immich_machine_learning     Started
 ✔ Container immich_server               Started

Check that all four containers started:

docker compose ps

You should see all four services with status Up or healthy:

NAME                     IMAGE                              STATUS
immich_server            ghcr.io/immich-app/immich-server   Up (healthy)
immich_machine_learning  ghcr.io/immich-app/immich-machin   Up (healthy)
immich_redis             docker.io/valkey/valkey:8-alpine   Up (healthy)
immich_postgres          ghcr.io/immich-app/postgres:14-v   Up (healthy)

The web UI is at http://your-server-ip:2283. On first load it walks you through creating the admin account. The first account registered becomes the admin. There’s no separate setup step for that.

Before you create it, decide how you want to structure accounts. Two approaches:

Admin account for daily use. One account, full access, photos and administration in the same place. Fine for a single-person setup. The downside: the API key you create for job scheduling (the nightly pause/resume script) is tied to your personal account, and the mobile app logs in with admin credentials.

Separate admin and personal accounts. Create a dedicated admin account (e.g. admin@home) for server administration and API keys, then create your personal account for photos. Your partner gets their own account too. The mobile app connects to the personal account, not the admin. More setup, cleaner separation.

Most people start with the first approach and it works fine. If you’re setting up accounts for multiple family members, the second is worth the extra minute.

Initial configuration

A few things to set before you invite anyone:

Storage template. By default Immich organizes uploads under library/{user}/{year}/{month}/. Take a look at this before you import anything. Changing it later triggers a full library reorganization on disk, which on a large library takes a long time and is nerve-wracking to watch. The default is fine for most setups. Check it once, then leave it alone.

Email (optional). Set up SMTP under Administration > System Settings > Email if you want to send shared album invitations by email. Without it, you share by link.

External library (optional). If you have existing photos already on disk, add them under Administration > External Libraries instead of re-uploading. Immich watches the directory and indexes without copying.

The ML service and CPU

Day-to-day, the ML service is invisible. New photos get indexed in the background within minutes of upload, and features like face recognition and semantic search just work. The CPU-only limitation only shows up once: the first time you sync a large existing library.

When we synced our phones for the first time (around 5,000 images combined) it pinned all cores for about 4 hours before the backlog cleared. It worked fine, just don’t expect to do video encoding on the same machine that afternoon. At half-core concurrency, budget roughly twice that.

If the Mac doubles as your workstation, two approaches help:

Why does the ML service run on CPU?

On Apple Silicon, GPU access requires native Metal support. Ollama gets this because it runs directly on macOS. Immich’s machine learning container runs inside OrbStack’s Linux VM, which has no access to Metal or the Neural Engine. There’s no workaround for containerized workloads. The CPU cores handle what a GPU would normally do on other platforms.

In practice this means face detection and CLIP embeddings run slower than they would on a discrete GPU. For a family photo library it’s plenty fast enough once the initial backlog is processed.

Throttle via job concurrency (optional)

In the Immich admin UI, go to Administration > Jobs. You’ll see each job type with a concurrency setting. A reasonable starting point is half your CPU core count. On a 10-core M1 Max that’s 5, leaving the other half available for workstation use:

Adjust down if the machine still feels sluggish. Indexing takes longer at lower concurrency, but the backlog does clear eventually.

Pause and resume on a schedule (optional)

For a tighter schedule (pause at 8 AM, resume at midnight), Immich has a job control API. It requires an admin API key, which you create under Account Settings > API Keys.

# Pause smart search indexing
curl -X PUT "http://your-server-ip:2283/api/jobs/smartSearch" \
  -H "x-api-key: YOUR_ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command": "pause", "force": false}'

# Resume it
curl -X PUT "http://your-server-ip:2283/api/jobs/smartSearch" \
  -H "x-api-key: YOUR_ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command": "resume", "force": false}'

A successful call returns 200 with a jobs status object. If you get 401, the API key is wrong or not from an admin account. If you get 400, the job name may have changed in your Immich version. Check the Swagger UI at http://your-server-ip:2283/api for the current names.

Job names: smartSearch, faceDetection, videoConversion, thumbnailGeneration. Wrap these in a launchd plist or cron job.

Hard CPU limit (optional)

If you want a hard ceiling regardless of job settings, add resource limits to the ML container in the compose file:

  immich-machine-learning:
    # ...existing config...
    deploy:
      resources:
        limits:
          cpus: '4.0'   # cap at 4 cores

A Mac Studio M1 Max has 10 CPU cores. Capping the ML container at 4 leaves 6 cores available for everything else. Blunter than job concurrency, but it works at the container level and doesn’t require API calls.

Check actual resource consumption

To see what Immich is using right now:

docker stats immich_machine_learning immich_server immich_postgres

During indexing, immich_machine_learning will sit near 100% of whatever CPU it has access to. Between indexing runs, when there are no new photos to process, it should be near 0%.

If you see the ML container consuming CPU continuously after the initial library indexing is done, check the job queue in the admin UI. Something might be stuck in a retry loop.

Mobile app setup

Download the Immich app on iOS or Android. The server URL is http://your-server-ip:2283.

Once logged in, enable Backup in the app settings and choose which folders to sync (Camera Roll, Screenshots, etc.).

Sync behavior: at home vs. away

When your phone is on the same network as the server, uploads go directly over the LAN. A GigE connection means a 10MB RAW file uploads in well under a second. This is the normal case for a home server.

When you’re away from home, the app can’t reach the server. The local IP isn’t routable from outside your network. Photos queue up on the phone and sync the moment you’re back on the LAN. For most households this is fine. The photos are safe on your phone; they’re just not on the server yet.

External access (VPN, reverse proxy) is out of scope for this guide. The base setup works on your local network.

Backup strategy

Database dumps

The Postgres database holds everything except the photo files: metadata, face tags, albums, sharing settings, and the ML embeddings. If the database gets corrupted, you lose all of that even if the photo files are intact.

Add a daily dump to your maintenance routine:

docker exec immich_postgres pg_dumpall \
  --username=postgres \
  > ~/server/data/immich/db-dumps/immich-$(date +%Y%m%d).sql

No output means success. Verify the file was written:

ls -lh ~/server/data/immich/db-dumps/
-rw-r--r--  1 yourname  staff   150M Mar  2 03:00 immich-20260302.sql

Keep 30 days of dumps and delete the rest. Each dump is usually a few hundred MB.

Photo files

The upload location (~/server/data/immich/library/) contains several subdirectories. Back up only library/library/. That’s where the originals live. Skip thumbs/, encoded-video/, and profile/, those are regenerable from the originals.

A vault-sync approach works well here: rsync the originals to an append-only backup destination on a schedule. The photo files don’t change after upload, so incremental syncs are fast.

The full backup writeup is in the Mac preparation guide.

Updating Immich

With IMMICH_VERSION=release, Immich doesn’t auto-update. You pull new images manually. Immich releases frequently and occasionally includes database migrations. Read the release notes before upgrading; breaking changes are flagged there.

To update:

cd ~/server/stacks/immich
docker compose pull
docker compose up -d

docker compose pull will list each image being updated:

[+] Pulling 4/4
 ✔ immich-server           Pulled
 ✔ immich-machine-learning Pulled
 ✔ redis                   Pulled
 ✔ database                Pulled

Database migrations run automatically on startup. Check the logs if anything looks off:

docker compose logs immich_server --tail=50

Checklist

Frequently asked questions

Can I import my Google Takeout into Immich? Download your Takeout archive, extract it, and point Immich’s external library at the folder. It indexes everything without copying. The annoying part is Google’s JSON sidecar files with the metadata. Community tools like immich-go handle that. We’ll cover the full migration in a separate guide.

How much storage do I need for Immich? Real numbers from our setup: ~16,000 files (mostly photos, ~1,400 videos) take 150GB in originals and 211GB total after Immich generates thumbnails and transcodes video. That 40% overhead is mostly video. A 4TB internal SSD handles this without thinking about it. If you have a huge video-heavy library, an external SSD for the upload location is an option.

Does Immich work with iPhone? The iOS app does automatic background upload over Wi-Fi. Camera Roll, Live Photos, videos, all of it. On the same network as the server, uploads go over LAN. A 10MB photo takes less than a second on a wired connection.


Next steps: Immich is running. Now set up your family: Immich for families: partner sharing, accounts, and getting everyone on board →

From the Build Log: Well, that escalated quickly →

We invested the time to perfect the setup. So you don't have to.

Check out famstack.dev →

Try it with your local LLM

Copy this guide and paste it into Open WebUI or any local chat interface as a new conversation. Your local model becomes a setup assistant that walks you through each step, explains commands, and helps troubleshoot errors.

I'm making this reusable for you.

Get notified when the repo goes online. One mail. Promise.