← All guides

How to Migrate Your Photo Library to Immich

Move years of family photos from NAS drives, old disks, and SD card dumps into Immich. Bulk import with dedup, junk filtering, and year albums.

This is from building famstack, the open-source home server stack I run for my family. Photos, documents, chat, local AI, all on a Mac in the corner.

Star it on GitHub →

What you’ll build: A clean, browsable photo library in Immich, imported from wherever your photos have been hiding: NAS shares, external drives, old laptop backups, grandma’s unsorted SD cards. Organized into year albums, junk archived out of sight, searchable by face and content, and accessible from every phone in your household. The photos from 2014 that nobody has looked at since the hard drive went into a drawer are on a timeline again.

Prerequisites:

This guide assumes you have famstack’s photos stacklet up and running, which runs Immich behind the scenes. The tooling here is built on top of the famstack CLI. Photos is part of famstack because that’s the real pain point in a family once you decide against Apple’s and Google’s clouds. The importer and album commands in this guide came out of my own migration, and they ship with the upcoming 0.3.0 release.

If Immich isn’t running on your Mac yet, start with Set Up Immich on Your Mac. If you want your family to see the results, set up partner sharing and accounts first.

#The problem

The typical starting point: Your phone has some of the photos, your partner’s phone has more, maybe a NAS you set up five years ago holds a few thousand you forgot about. There’s a folder called Pictures on an external drive that hasn’t been plugged in since 2021. Your parents gave you a USB stick with photos from a family reunion. And somewhere in all of that are the only copies of your kid’s first birthday.

Nobody has looked at most of these photos in years, and it’s not for lack of caring. They’re buried. You’d need to plug in the right drive, navigate to the right folder, remember which year it was, and hope the folder names make sense. The most important memories your family has are scattered across drives nobody plugs in. That was my starting point, and the reason I started famstack in the first place. My root pain point, so to speak.

Immich solves the viewing and sharing part, but only if the photos are actually in it. Getting them there is the migration, and that’s what this guide covers. Hosting your own photo library at home also raises questions this guide skips, like keeping the Mac running reliably and what happens when a disk dies. Setting up your Mac as a home server and the backup strategy guide cover that groundwork.

#Strategy: year by year, not all at once

The temptation is to point an import tool at your entire photo archive and let it rip. Don’t do that.

A 200GB import can take hours. If something goes wrong halfway through (a network hiccup, a permissions issue, a false positive in duplicate detection), you’re left wondering which photos made it and which didn’t. Immich’s ML service can also peg your CPU for hours while it indexes faces and generates embeddings for thousands of photos at once. Your Mac becomes unusable.

Instead, import year by year. One folder at a time. Check the results in Immich after each batch. Clean up junk before moving to the next year. This takes more calendar time but far less actual effort, and you end up with a cleaner library.

The workflow for each batch:

  1. Scan. Point the importer at a source folder. Check what would be imported.
  2. Upload. Check for duplicates. Send a deduplicated chunk to Immich.
  3. Clean. Archive WhatsApp junk, remove it from albums.
  4. Organize. Move photos into year-based albums.
  5. Verify. Check the results in the Immich app.

#Step 1: Mount your source

Plug in the drive, mount the NAS share, or unzip the Google Takeout export. You need a path on your Mac that points to the photos.

Common setups:

SourceTypical path
External drive/Volumes/MyDrive/Photos
NAS (SMB)/Volumes/nas/photos
SD card/Volumes/Untitled/DCIM
Old laptop backup/Volumes/Backup/Users/me/Pictures

One source that does NOT belong in this list: Google Takeout. Takeout exports need special handling because of how Google stores metadata. See the Takeout section below.

Make sure the drive stays mounted during the import. If you’re pulling from a NAS over Wi-Fi, consider Ethernet.

#Step 2: Scan and build a manifest

Tell the importer where your photos are. It scans the folder, finds every media file (photos and videos), and builds a manifest, a list of everything it plans to upload.

./stack photos import --source /Volumes/nas/Photos/2018

Output:

  Scanning /Volumes/nas/Photos/2018...

  Manifest built:
    Source:            /Volumes/nas/Photos/2018
    Media files found: 3847

  Run 'stack photos import --proceed N' to start uploading.

Nothing has been uploaded yet. The importer just catalogued what’s there. You can check the status at any time:

./stack photos import --status

This shows every source you’ve scanned, how far along each one is, and what’s left.

#Deduplication

Is your photo library a bit messy and unstructured? Don’t worry, mine was too. I wasn’t 100% sure my folders were free of duplicates, because some years I created monthly sub-folders, some years I didn’t, and a few photos ended up in more than one directory. That’s why the importer runs two layers of duplicate detection:

  1. Local hash ledger. Every file you’ve ever imported gets its SHA-256 hash recorded. If you scan a folder that overlaps with a previous import, duplicates are skipped instantly without touching the server.
  2. Immich server-side. Even if a file passes the local check, Immich compares it against everything already in the library. Photos uploaded from your phone won’t be duplicated.

This means you can safely scan overlapping folders, reimport the same source, or point the importer at a parent directory that contains subdirectories you’ve already done. Duplicates are handled.

If you know there are no duplicates and want to skip the hash calculation (which can be slow on large folders over network mounts), use --force:

# Upload the next 500 files from the current source, with the local duplicate check
./stack photos import --proceed 500

# Same, but skip the local hash step
./stack photos import --proceed 500 --force

Immich still deduplicates server-side, so you won’t get actual duplicates. You just skip the local hash step.

#Step 3: Upload your photos in batches

Start uploading. The --proceed flag controls how many files to send in one batch:

./stack photos import --proceed 200

Start with a small batch (50–100) for your first import to make sure everything works. Once you’re confident, go larger. The cursor tracks your position, so you can stop and resume at any time, even across reboots.

# Upload 100, go check the results, come back later
./stack photos import --proceed 100

# Continue where you left off
./stack photos import --proceed 500

All uploads land in an album called “Imported” by default. You can change this:

./stack photos import --proceed 200 --album "2018"

#How to import an unsorted photo library into year albums

If your source is a big unsorted dump with years of photos in one flat folder, use --album-per-year. The importer reads the EXIF date from each file and creates one album per year automatically.

Here’s a real import of a folder with 6,023 photos from a NAS:

./stack photos import --source /Volumes/files/Bilder/2015
  Scanning /Volumes/files/Bilder/2015...

  Manifest built:
    Source:            /Volumes/files/Bilder/2015
    Media files found: 6023

  Run 'stack photos import --proceed N' to start uploading.

Upload the first 2,000 with year bucketing:

./stack photos import --proceed 2000 --album-per-year
  Processing 2000 files (#1 – #2000 of 6023):

    hashed 1000/2000...
    hashed 2000/2000...
    2000 new, 0 duplicates skipped

  Extracting years...
    2015: 1999 files
    2016: 1 files

  Album "2015" (1999 files):

  Uploading 1999 files to album "2015"...

    chunk 1: 200 files
    staged 200 files to /var/folders/.../immich-import-imhrrnvu
    immich: Logging in to http://stack-photos-server:2283
    immich: Found 200 new files and 0 duplicates
    immich: Successfully uploaded 200 new assets (1.2 GB)
    immich: Successfully created 1 new album
    ...

The importer read the EXIF dates from 2,000 files, found that 1,999 were taken in 2015 and one straggler from 2016, created albums for each year, and started uploading in chunks of 200. The cursor advances after each batch, so stopping mid-import loses nothing.

The year comes from EXIF DateTimeOriginal when available (via exiftool in batch mode, which stays fast even for thousands of files). Files without EXIF data fall back to filename patterns like 20180415_120000.jpg or IMG-20180415-WA0032.jpg. Files where no year can be determined at all go into an “Unknown Year” album that you can sort manually later.

Preview the year distribution without uploading:

./stack photos import --proceed 2000 --album-per-year --dry-run

#Switching sources

Each source path gets its own cursor. You can work on multiple sources in parallel:

# Start with 2018
./stack photos import --source /Volumes/nas/Bilder/2018
./stack photos import --proceed 500

# Switch to 2019
./stack photos import --source /Volumes/nas/Bilder/2019
./stack photos import --proceed 500

# Go back to 2018, picks up where you left off
./stack photos import --source /Volumes/nas/Bilder/2018
./stack photos import --proceed 500

#Step 4: How to clean WhatsApp junk out of your timeline

Phone backups come with WhatsApp baggage: the “good morning” images your aunt sends every day, memes, forwarded photos of photos. They all have filenames like IMG-20200412-WA0032.jpg and they pollute your timeline.

Preview the damage first, scoped to the album you just imported:

./stack photos album archive --from "2015" --pattern whatsapp --dry-run
  Album: 2015
  47 assets matching 'IMG-*-WA*'

  Sample:
    IMG-20150312-WA0006.jpg
    IMG-20150315-WA0000.jpg
    IMG-20150420-WA0011.jpg
    ... and 37 more

  Dry run — no changes made.

47 WhatsApp forwards hiding among 1,999 real photos. Remove the --dry-run to archive them:

./stack photos album archive --from "2015" --pattern whatsapp

Archiving does three things:

  1. Finds every asset whose filename matches the WhatsApp pattern (IMG-*-WA*)
  2. Sets their visibility to “archived”, so they disappear from your timeline but stay in the library
  3. Detaches them from any albums they were in

Nothing is deleted. The assets are still searchable in the archive view and still backed up, just not in the way anymore. If you want to keep the album memberships (archive without detaching), add --keep-albums.

Drop the --from to sweep the entire library instead of one album:

./stack photos album archive --pattern whatsapp --dry-run
  Searching assets matching 'IMG-*-WA*'...
  1633 assets match 'IMG-*-WA*'

  Sample:
    IMG-20250917-WA0007.jpg
    IMG-20231225-WA0003.jpg
    IMG-20200412-WA0032.jpg
    ... and 1623 more

  Scanning albums...
  Will detach from 1 album(s):
    Imported (1633 assets)

  Dry run — no changes made.

Archive instead of delete, because WhatsApp images are a mix of junk and things you actually care about. Most of them are memes and forwarded “good morning” images. But buried in there is the photo your mom sent of the grandkids, or the scan of a handwritten recipe. Archiving moves them all out of the way first, so your timeline is clean. Then you can browse the archive at your own pace, pull out the keepers, and add them back to an album. Unarchiving is one command:

./stack photos album unarchive --pattern whatsapp

Immich runs face recognition on every photo, including the archived ones, which opens up a smarter version of this:

./stack photos album unarchive --pattern whatsapp --top-faces

This would check Immich’s face data and automatically unarchive any WhatsApp image that contains one of the most frequently occurring faces in your library, which in practice means your family. The memes stay buried, but the keepers come back to the timeline on their own. (Not implemented yet, but the data is there and the API supports it.)

#Other junk patterns

The --pattern flag takes any glob pattern. Clean up whatever you don’t want on your timeline:

./stack photos album archive --pattern '*.gif'          # animated GIFs
./stack photos album archive --pattern 'Screenshot*'    # screenshots
./stack photos album archive --pattern 'signal-*'       # Signal downloads

You can also scope the archive to a specific album instead of searching the entire library:

./stack photos album archive --from "Imported" --pattern '*.gif'

#Step 5: Organize into albums

After importing and cleaning, your photos are in the “Imported” album. Move them into year-based albums:

./stack photos album move --from "Imported" --to "2018"

This copies every asset from “Imported” into “2018” (creating the album if it doesn’t exist), then removes them from “Imported”. One command, one pass.

If “Imported” contains photos from multiple years and you want to be selective, use --pattern:

./stack photos album copy --from "Imported" --to "2018" --pattern '2018*'

Or work with individual commands for more control:

# Copy specific photos
./stack photos album copy --from "Imported" --to "Vacation 2018"

# Remove from source after verifying
./stack photos album clear --from "Imported"

All album commands support --dry-run to preview before committing.

#The full workflow

There are two approaches depending on how your photos are organized on disk.

#Already sorted by year

If your source has folders like 2018/, 2019/, etc., import one year at a time:

# 1. Scan the source
./stack photos import --source /Volumes/nas/Bilder/2018

# 2. Preview first
./stack photos import --proceed 100 --dry-run

# 3. Upload everything
./stack photos import --proceed 5000

# 4. Archive WhatsApp junk
./stack photos album archive --pattern whatsapp

# 5. Move to a year album
./stack photos album move --from "Imported" --to "2018"

# 6. Check the results in the Immich app, then move to the next year
./stack photos import --source /Volumes/nas/Bilder/2019

Repeat for each year. Do 2018 on Monday, 2019 on Tuesday. The cursors remember where you are.

#One big unsorted folder

If everything is in one flat folder with years of photos mixed together, let the importer sort it for you:

# 1. Scan the source
./stack photos import --source /Volumes/backup/AllPhotos

# 2. Preview the year distribution
./stack photos import --proceed 500 --album-per-year --dry-run

# 3. Import with auto-bucketing
./stack photos import --proceed 5000 --album-per-year

# 4. Archive WhatsApp junk across all albums
./stack photos album archive --pattern whatsapp

The importer reads EXIF dates, groups files by year, and creates one album per year automatically. Photos without EXIF data fall back to filename patterns. Anything without a detectable year lands in “Unknown Year” for manual sorting later.

This turns a decade of chaos into a browsable library in one command.

#How to import Google Takeout into Immich

Everything above assumes your photos are ordinary files with their metadata inside them. Google Takeout breaks that assumption. When you export from Google Photos, Google moves parts of the metadata (edited dates, descriptions, location edits, album membership) out of the image files and into JSON sidecar files sitting next to them. Import the images alone and you may lose exactly the information that makes a library browsable.

Don’t run this guide’s importer over a Takeout export. Use immich-go instead. It’s a community tool built for this one job: point its from-google-photos command at the Takeout zip files (no need to extract them), and it matches photos with their JSON sidecars, restores the metadata, and recreates your Google Photos albums in Immich. The importer may learn to handle Takeout natively at some point, but immich-go does this well today.

The division of labor:

Your photos are…Use
Folders on a NAS, external drive, or SD cardThis guide’s importer
Copied off a phone into some folderThis guide’s importer
A Google Takeout exportimmich-go

The two combine fine. Immich deduplicates server-side, so running immich-go for your Takeout and this importer for everything else won’t create duplicates, even where the sources overlap.

#After the migration

Once your library is in Immich, a few things happen automatically:

  • Face recognition clusters your family’s faces across the entire library. Search for your daughter’s name and find every photo she’s in, going back years.
  • Semantic search lets you search by content (“beach”, “birthday cake”, “dog”) without any manual tagging.
  • Memories surface photos from this day in previous years. The photos that were buried on a NAS for half a decade now show up on your phone as a morning notification.
  • Partner sharing merges both partners’ timelines. Set it up once and every photo from either phone appears in one chronological view.

The photos that used to be a folder on a drive that nobody plugged in anymore are now on every phone in your household. Your kids can scroll through their own baby photos. Your partner can find that vacation photo from 2019 without asking you which drive it’s on.

The server and the import scripts are just plumbing. The point is that your family’s photos are alive again.

#Sharing with your family

Once your library is imported and organized, set up sharing so everyone benefits from it. Partner sharing merges two timelines into one. Shared albums let you build collections for specific people: grandparents, extended family, friends.

The Immich for Families guide covers partner sharing, account setup, mobile app configuration, and the one trick that actually gets your family using it instead of it being “your thing.”

#Migration checklist

Per source, in order:

  • Source mounted and reachable (ls the folder)
  • Manifest built (--source, check with --status)
  • Small test batch uploaded and verified in the Immich app
  • Remaining files uploaded (--proceed, with --album-per-year for unsorted folders)
  • WhatsApp and other junk archived (--pattern)
  • Photos in year albums, “Imported” empty
  • Spot-checked the year albums in the app before starting the next source

#Frequently asked questions

Can I import Google Takeout into Immich? Yes, but not with a plain file importer. Google moves metadata (edited dates, descriptions, album membership) into JSON sidecar files, and importing just the images can lose it. Use immich-go, which reads Takeout zips directly and merges the sidecar metadata back in. See the Takeout section above.

Does Immich detect duplicate photos during import? Yes, server-side by file hash, so re-uploading a photo that’s already in the library is a no-op. The importer adds a local hash ledger on top, which means overlapping folders and repeated scans get skipped before anything touches the network.

How is this different from immich-go or the official Immich CLI? Different jobs. immich-go is the right tool for Google Takeout exports because it understands Google’s JSON sidecars. This guide’s importer is for everything else: the NAS folder dump, the images copied off an old phone, the unsorted SD cards. It adds a resumable cursor across sources, automatic year albums from EXIF dates, and pattern-based junk archiving after the upload. If you have a decade of photos spread over five drives, the cursor and the cleanup commands are the difference between a weekend project and an abandoned one.

#Quick reference

CommandWhat it does
./stack photos import --source <path>Scan a folder, build manifest
./stack photos import --statusShow all sources and progress
./stack photos import --proceed NUpload next N files
./stack photos import --proceed N --forceUpload without local hash dedup
./stack photos import --proceed N --album "Name"Upload into a specific album
./stack photos import --proceed N --album-per-yearAuto-bucket into year albums
./stack photos import --proceed N --dry-runPreview without uploading
./stack photos album archive --pattern whatsappArchive WhatsApp images
./stack photos album archive --from "Album"Archive an entire album
./stack photos album copy --from "A" --to "B"Copy assets between albums
./stack photos album clear --from "A"Remove all assets from an album
./stack photos album move --from "A" --to "B"Copy + clear in one pass

All commands accept --dry-run to preview and --api-key for a specific user’s key.

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 a builder. I don't email often.

When I publish something worth your time, you get one email. Unsubscribe anytime.