Jacob Kaplan-Moss

Today I Learned…

My Immich Setup

Notes (mostly for myself but maybe useful to others?) on how I set up Immich for storing/archiving photos.

Goals:

Consolidate all my photos – old archives off a NAS, a couple of years of photos stored in Google Photos, and ongoing photos taken with my iPhone – into a self-hosted Immich instance running on my NAS (a Synology). iCloud remains as a capture tool; Immich is the storage, organization, and browsing interface.

Goals: Consolidate all photos — old NAS archives, Google Photos, and ongoing iPhone captures — into a self-hosted Immich instance on the Synology NAS. iCloud/Photos remains the primary mobile capture tool; Immich is the long-term archive and browsing interface.

Tools

  • Immich - duh
  • osxphotos - export photos out of Photos.app / iCloud
  • immich-go - bulk upload photos to Immich (I found this did what I wanted out of the box better than the first-party immich-cli)

Immich setup and installation

Streightforward, following the Immich community-maintained Synology installation guide - this installs Immich as a Docker Compose project via Synology’s Container Manager interface.

My setup is 95% as-documented, with a few key config changes and differences:

  • I pointed Immich’s built-in library to /volume1/photo - a dedicated photo drive - rather than the suggested default of the Docker directory (/volume1/docker on my NAS). This is so I can have the large photo library separate from all my various Docker detritus, mostly so I can set up different backup schedules for the different items.

  • I configured Immich with a Storage Template to store photos in a date heirarchy. This is so that if something goes terribly wrong, I’ll still have a somewhat-navigable photo library just on the file system.

  • I have a few tools I’m building on top of Immich (not covered here, may write about these later). These are JavaScript tools, and thus need Immich to send CORS headers to be able to consume the Immich API. Immich doesn’t have anything built-in to do this, so for me the easiest way to make this happen was with a Caddy sidecar; here’s the relevant docker-compose.yml parts:

    services:
      caddy:
        container_name: immich_caddy
        image: caddy:2-alpine
        restart: always
        ports:
          - "2283:2283"
        volumes:
          - ./Caddyfile:/etc/caddy/Caddyfile:ro
        depends_on:
          - immich-server
    
      immich-server:
        # as documented, except for ...
        expose: ["2283"] # ... instead of port mapping
    

    And the Caddyfile:

    :2283 {
            reverse_proxy immich-server:2283
            header {
                    Access-Control-Allow-Origin *
                    Access-Control-Allow-Methods *
                    Access-Control-Allow-Headers *
            }
            @options method OPTIONS
            respond @options 204
    }
    

One-time imports

Import raw photo directories:

immich-go upload from-folder \
    --server=https://im.jacobian.me/ \
    --api-key=$(op read op://personal/immich-go/credential) \
    --manage-burst Stack \
    photos-from-old-nas-to-import/

For Google Photos, I requested an export through the “Takeout” interface, which took a couple of days. immich-go is supposed to be able to operate on the raw zip files that you get from Takeout, but that didn’t work for me. No problem, I just unpacked them, and ran:

immich-go upload from-google-photos \
    --server=https://im.jacobian.me/ \
    --api-key=$(op read op://personal/immich-go/credential) \
    --manage-burst Stack \
    gphotos/Takeout

Ongoing sync from iCloud

Getting photos out of iCloud is easy: osxphotos works great, and has all sorts of options. There are several ways I could import those photos into Immich, depending on the workflow I want. I considered ditching iCloud entirely; if I’d done that, I would have uploaded my photos into Immich using immich-go upload from-folder or expored the upload from-icloud option.

However, I decided I want to keep using iCloud: there are aspects of the workflow I like (namely: photos transparantly syucing to all devices; easy sharing with family members), and I’ve read several accounts of the Immich mobile app being flaky, and my photo library isn’t big enough to make my iCloud spend a problem. So, the approach I’m taking is that iCloud stays as the primary way photos get taken and synced to my Mac, and then I’ll periodically back them up into Immich for archival, organization, publication, etc.

I decided the best way to do this is to export photos from iCloud using osxphotos on to my NAS, and point Immich at that directory as an External Library. I wasn’t sure I’d be able to figure out incremental uploads into Immich’s built-in photo library – but osxphotos does incremental exports, so this works nicely. The one tradeoff is that if I make edits to photos in Photos.app or on my phone, those won’t get synced to Immich. I’m OK with that: that’s not a thing I do except very very rarely.

So, here’s the little wrapper around osxphotos that does the export:

#!/usr/bin/env -S uv run --script
from pathlib import Path
from subprocess import run

HERE = Path(__file__).parent.absolute()

def main():
    # mount the network share
    run(["osascript", "-e", 'mount volume "smb://jacob@ds2/photo"'])

    run(
        [
            "osxphotos", "export", "/Volumes/photo/ios-photos",
            "--export-by-date",                   # organize exported photos into folders by date
            "--skip-original-if-edited",          # just export  edited versions of photos
            "--download-missing",                 # download full-res versions from icloud
            "--use-photokit",                     # "experimental" downloader, per docs, but more robust?
            "--exportdb", (HERE/"osxphotos.db"),  # store export state db here (be nice to NAS)
            "--retry", "3",                       # retry failed exports 3x
            "--sidecar", "XMP",                   # XMP "sidecars" (metadata) - immich reads these
            "--fix-orientation",                  # rotate images rotated in iphotos
            "--update",                           # incrementally update export
            "--only-new",                         # only export new files, not udpated ones
            "--added-in-last", "7d"               # not strictly necessary, but speeds things up
        ]
    )

if __name__ == "__main__":
    main()

I have this set up to run nightly on my Mac, and Immich set up to re-scan the external library about 3 hours later.

Next steps:

The one missing piece is shared Google Photos libraries: frequently, after a group trip, someone will set up a Google Photos album to share photos. I’d like to export those photos into Immich, but at a cursory glance it’s not super streightfoward to do this in a fully-automated way (Google Photos has a pretty limited API for shared albums).

The other planned next step is a triage and publication workflow. The whole point of getting images into Immich was to have a foundation for building my own tooling/workflow on top of it. Key to that is a way to quickly triage albums down to a concise subset, and publish them. I have plans here, and an intiial proof of concept, but nothing I’m ready to share/talk about yet.