← 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 working Immich instance on your Mac. Upload some photos, try the mobile app, see how face recognition and semantic search feel before committing to it.

End state: Immich running as four Docker containers, accessible from any browser on your network, with the mobile app syncing photos from your phone. Good enough to kick the tires, not ready for “this is where all our family photos live now.” For that you’d still need a backup strategy, a proper URL instead of IP:2283, HTTPS, and remote access. This guide gets you to the point where you can decide if Immich is worth that investment.

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

This guide assumes macOS on Apple Silicon (M1 or later). If you haven’t configured your Mac for server use yet (sleep disabled, static IP, auto-restart after power failure), the Mac preparation guide covers all of that. Not strictly required for getting Immich running, but it’ll save you from debugging the same things I did.

Every command in this guide runs in Terminal. Open it from Applications > Utilities > Terminal, or press Cmd+Space and type “Terminal”. All file paths starting with ~/ refer to your home directory (e.g. /Users/yourname/).

#OrbStack: Docker for Mac, but better

You need a way to run Docker containers on macOS. That means either Docker Desktop or OrbStack. Use OrbStack.

Docker Desktop works, but it’s heavier than it needs to be. OrbStack’s Linux VM starts faster, uses less memory, and file sharing between macOS and containers is noticeably quicker. Free for personal use. I switched early and haven’t thought about it since.

Download it from orbstack.dev and drag it into Applications like any other Mac app. If you prefer Homebrew: brew install orbstack.

Open OrbStack once from your Applications folder. It runs a quick setup and adds itself to your menu bar. From that point on, the docker and docker compose commands work in your terminal.

Verify Docker is available:

docker ps

You should see an empty table:

CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

If you get command not found, open OrbStack from Applications and wait for it to finish its initial setup.

#Create the Docker network

The containers in this stack communicate over a shared Docker network. Create one called home:

docker network create home

You should see a long hash printed back (the network ID):

a1b2c3d4e5f6...

This only needs to happen once. If you already created it for another stack, you’ll get a message saying the network already exists. That’s fine, move on.

#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. The official Immich Docker Compose reference always lists the current recommended image tags.

#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

The -p flag creates any missing parent directories along the way, so you don’t need to create each level separately. 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.

#Using an external SSD instead

If your Mac’s internal storage is limited, you can keep Immich’s data on an external USB-C SSD (Samsung T7/T9, SanDisk Extreme, etc.). Check where macOS mounted it with ls /Volumes/, then use that path instead of ~/server/data/immich/ for the directories above:

mkdir -p /Volumes/MySSD/immich/library
mkdir -p /Volumes/MySSD/immich/postgres
mkdir -p /Volumes/MySSD/immich/db-dumps

Update the .env file (covered below) to point at these paths. The Postgres database should ideally live on an SSD. A spinning HDD can work for the photo library, but the database benefits from SSD speeds.

Two things to watch: disable disk sleep in System Settings > Energy (or sudo pmset -a disksleep 0) so macOS doesn’t unmount the drive while containers are running. And verify the drive remounts after a reboot (ls /Volumes/), because if it doesn’t, the database corrupts on next start.

#Docker Compose

Think of Docker Compose as a recipe card. It lists the ingredients (container images), the quantities (how much CPU, which ports), and the order of steps (start the database before the server). You write the recipe once, and docker compose up cooks the whole thing. If you’re installing Immich months after this guide was written, check the Immich docs for any changes to the recommended compose configuration.

Create the directory for your Immich stack, then create the compose file inside it:

mkdir -p ~/server/stacks/immich

Create ~/server/stacks/immich/docker-compose.yml with the following contents. You can use any text editor (VS Code, Sublime Text), or create it from the terminal with nano:

nano ~/server/stacks/immich/docker-compose.yml

Paste the YAML below, then save with Ctrl+O, press Enter, and exit with Ctrl+X:

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:

  • /etc/localtime:ro: shares the host timezone with the container so photo timestamps don’t land in UTC.
  • model-cache volume: stores the ML models Immich downloads on first start. CLIP and the face detection model together are around 1GB. Don’t delete this volume.
  • IMMICH_VERSION=release: tracks the latest release automatically. Fine for most setups, but if you want stability, pin to a specific version after reading the changelog: IMMICH_VERSION=v1.125.0.

You’ll notice ${UPLOAD_LOCATION} and ${DB_DATA_LOCATION} in the compose file. These are variables, not actual paths. Docker reads their values from the .env file below. This is where you tell Immich where your photos and database actually live on disk. These must match the directories you created in the Directory layout step. Get these wrong and Immich either can’t find your photos or writes the database to the wrong place.

#Environment configuration

Create a file called .env in the same directory as your compose file. Files starting with a dot are hidden in Finder by default. Press Cmd+Shift+. to toggle their visibility.

The two most important values here are UPLOAD_LOCATION (where every photo and video ends up) and DB_DATA_LOCATION (where Postgres keeps its data). Replace /Users/yourname/ with your actual home directory path:

# Where uploaded photos and videos are stored
# For external SSD, use /Volumes/YourSSD/immich/library
UPLOAD_LOCATION=/Users/yourname/server/data/immich/library

# Where Postgres stores its data (should be on SSD for performance)
# For external SSD, use /Volumes/YourSSD/immich/postgres
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
# The database is only reachable from other containers on the Docker
# network, never exposed to the outside. A simple password is fine here.
DB_PASSWORD=immich
DB_USERNAME=postgres
DB_DATABASE_NAME=immich

#Start the stack

Navigate to your stack directory:

cd ~/server/stacks/immich

On the first run, Docker pulls all the container images. This takes a few minutes depending on your connection (the images are several hundred MB each).

Start with a test run in the foreground so you can watch what’s happening:

docker compose up

Without the -d flag, all container logs stream into your terminal. You’ll see Postgres initializing, the ML service downloading its models (about 1GB for CLIP and face detection), and the main server starting up. It’s a lot of output. Look for lines like Immich Server is listening on 0.0.0.0:2283 from the server container, that means it’s ready.

If something goes wrong, you’ll see the error right there in the logs. Press Ctrl+C to stop everything.

Once it’s running cleanly, stop the test run with Ctrl+C and restart in the background:

docker compose up -d

The -d flag runs everything detached. No log output in your terminal, containers keep running after you close the window.

You should see:

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

Verify all four are healthy:

docker compose ps
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)

If any container shows Restarting or Exit instead of Up, check the logs:

docker compose logs immich-server --tail=30

Common issues on first start:

  • “network home not found”: The Docker network step was missed. Run docker network create home and try again.
  • Postgres errors about permissions: The data directory was likely auto-created by Docker as root. Remove it (rm -rf ~/server/data/immich/postgres), recreate it with mkdir, and restart.
  • ML container keeps restarting: It needs to download about 1GB of models on first start. If your connection is slow, the health check can time out and restart the container. It usually sorts itself out after a couple of restarts. Check with docker compose logs immich-machine-learning --tail=10.

The web UI is at http://your-server-ip:2283. If you’re sitting at the Mac, http://localhost:2283 works. From another device on the same network, use the Mac’s IP address. To find it:

ipconfig getifaddr en0

You should see something like 192.168.1.42. If that returns nothing, try en1 instead (depends on whether you’re on Ethernet or Wi-Fi). You can also find it in System Settings > Network.

On first load the web UI 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:

  • Set Smart Search concurrency to 5
  • Set Face Detection concurrency to 5
  • Set Video Conversion concurrency to 5

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.

#Accessing Immich from other devices

Before you try connecting from your phone or another computer, make sure the Mac’s firewall isn’t blocking connections. The macOS firewall is off by default, but if you turned it on, OrbStack needs to be allowed through.

Check under System Settings > Network > Firewall. If it’s on, click Options and make sure OrbStack is in the list of allowed apps. If it’s not there, add it. Without this, other devices on your network can’t reach the Immich web UI or API.

Test from another device first: open a browser on your phone or another computer and go to http://your-server-ip:2283 (use the IP from the ipconfig getifaddr en0 command earlier). If you see the Immich login page, you’re good.

#Mobile app setup

Install the Immich app from the App Store (iOS) or Google Play (Android).

On the login screen, enter:

  • Server URL: http://your-server-ip:2283 (the same IP you used in the browser, e.g. http://192.168.1.42:2283)
  • Email / Password: the account you created in the web UI

After logging in, go to the backup settings. On iOS, tap your avatar in the top corner, then Backup. On Android, tap the cloud icon at the top. For a first test, don’t select your entire Camera Roll. Pick a small album or just WhatsApp Images. You can always add more later, but syncing a full photo library on the first run means hours of ML indexing and no easy undo if you decide to start fresh. Once you’re happy with how everything works, come back and enable the albums you actually want.

When you’re away from home, the app can’t reach the server. Photos queue up on the phone and sync the moment you’re back on your home network. For most households this is fine. The photos stay safe on your phone until the next sync. Remote access is covered in Before you go to production.

#Before you go to production

This guide gets Immich running for testing. Before you make it the permanent home for your family’s photos, there are a few things between “it works on my network” and “I trust this with 50,000 irreplaceable photos”:

Backups. Immich has a built-in database dump that protects against failed update migrations, but those dumps live on the same disk as everything else. If the disk dies, they go with it. A real backup strategy means external drives or offsite storage. Time Machine on a separate drive is already a decent safety net (covers accidental deletion, bad updates, disk corruption), but it won’t help with theft or fire. The Mac preparation guide touches on the options.

A proper URL. Right now you’re accessing Immich at http://192.168.1.42:2283. That works, but it’s not something you’d hand to your partner. A reverse proxy (Caddy, Traefik) gives you a clean address like photos.home or photos.yourdomain.com and handles HTTPS.

HTTPS. Without it, photos travel over your network unencrypted and the mobile app shows a security warning on every login. A reverse proxy with a local or public certificate fixes both.

Remote access. When you leave the house, the app can’t reach the server. Tailscale is the lowest-friction option: install it on the Mac and your phone, and Immich is reachable from anywhere without exposing ports to the internet.

Each of these deserves its own guide. For now: Immich running is step one, not the finish line.

#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

  • OrbStack installed and running
  • Docker network home created
  • Data directories created (library/, postgres/, db-dumps/)
  • .env configured with correct paths and timezone
  • Test run passed (docker compose up without -d, no errors)
  • All four containers healthy (docker compose ps)
  • Admin account created in web UI
  • Mobile app installed and backup enabled
  • Backup strategy planned (see Mac preparation guide)
  • Verified photos accessible at home (LAN URL)

#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 →

The big picture: You Bought a Mac Mini. Now What? → — photos are just the beginning.

From the Build Log: Well, that escalated quickly →

Want the fast path? famstack automates all of this. Immich, reverse proxy, config, the lot.

git clone https://github.com/famstack-dev/famstack
cd famstack
./stack install
./stack up photos

That’s it. Immich running, mobile-ready.

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

Check out famstack.dev →

Hi, I'm Arthur 👋 Interesting? Stuck? Got improvements? Come yell at me or just say hi.

I'm making this reusable for you.

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