Skip to main content
  1. Posts/

Synology Photos → Immich Migration Runbook

·995 words·5 mins
Author
Yang Hu

Personal runbook for migrating a family photo library from Synology Photos to a self-hosted Immich instance. Covers bulk upload, Google Takeout import, and album reconstruction via the Synology PostgreSQL database.

Setup
#

  • Source: Synology NAS running Synology Photos (multiple users)
  • Destination: Immich self-hosted on the same NAS
  • Upload tool: immich-go v0.31+
  • Client: WSL2 on Windows, SSH access to NAS
  • Album script: custom Python (migrate_albums.py) using Immich REST API

Phase 1: Photo Uploads
#

Strategy
#

Two sources per user:

  1. Google Takeout — photos before the cutoff date (when Google Photos was primary)
  2. Synology folder — photos after the cutoff date (when Synology became primary)

The cutoff date is when you switched from Google Photos to Synology as your primary photo storage. Photos before that date live in Google Photos at full resolution; after that date, full-resolution is on Synology.

immich-go from-folder upload
#

Run on the NAS directly — avoids copying large libraries (100 GB+) over the network.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ssh nas
cd /path/to/immich-docker

nohup ./immich-go upload from-folder \
  --server=http://<nas-ip>:2283 \
  --api-key=<user-api-key> \
  --concurrent-tasks=6 \
  --manage-raw-jpeg=StackCoverJPG \
  --manage-burst=Stack \
  --pause-immich-jobs=true \
  --session-tag \
  --on-errors=continue \
  /path/to/photos \
  > upload.log 2>&1 &

Key flags:

FlagPurpose
--manage-raw-jpeg=StackCoverJPGStack RAW+JPEG pairs, JPEG as cover
--manage-burst=StackStack burst photo sequences
--pause-immich-jobs=truePause ML jobs during upload (faster)
--session-tagTag all uploads with session ID for tracking
--concurrent-tasks=6Parallelism — tune to your NAS CPU
--on-errors=continueDon’t abort on individual file failures

To upload a folder directly as an album:

1
2
3
4
5
6
./immich-go upload from-folder \
  --server=http://<nas-ip>:2283 \
  --api-key=<user-api-key> \
  --into-album="Album Name" \
  /path/to/folder \
  > upload.log 2>&1 &

Google Takeout import
#

Download the Takeout archive(s), then:

1
2
3
4
5
./immich-go upload from-google-photos \
  --server=http://<nas-ip>:2283 \
  --api-key=<user-api-key> \
  --create-albums \
  /path/to/takeout-*.zip

Use --date-range=<start>,<cutoff> to limit to photos before the cutoff date (avoids importing lower-resolution Google-compressed copies of photos you already have on Synology).

Phase 2: Album Reconstruction
#

Synology Photos stores album membership in its PostgreSQL database. After photos are in Immich, you reconstruct albums via the Immich REST API using a script that reads a TSV export from Synology’s DB.

Export album data from Synology DB
#

Synology Photos uses PostgreSQL. Database: synofoto. Must run as postgres user (peer auth — no password, but requires sudo).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# On NAS terminal:
sudo su -s /bin/sh postgres -c "psql synofoto -A -F$'\t' -t -c \"
SELECT a.id, a.name, a.shared, ui.name, u.id_user, ui2.name,
  f.name, u.filename, u.takentime, u.duplicate_hash
FROM public.album a
JOIN public.normal_album na ON na.id = a.id
JOIN public.many_item_has_many_normal_album mia ON mia.id_normal_album = na.id
JOIN public.item i ON i.id = mia.id_item
JOIN public.unit u ON u.id_item = i.id
JOIN public.folder f ON f.id = u.id_folder
JOIN public.user_info ui ON ui.id = a.id_user
JOIN public.user_info ui2 ON ui2.id = u.id_user
ORDER BY a.id, f.name, u.filename;
\"" > /volume1/homes/<admin-user>/album_export.tsv

# Copy to workstation:
scp nas:/volume1/homes/<admin-user>/album_export.tsv ./album_export.tsv

Note: Old psql on Synology does not support --csv. Use -A -F$'\t' -t for tab-separated output.

Key tables:

TablePurpose
albumAlbum metadata (name, owner, shared flag)
normal_albumMarks album as regular (non-smart) type
many_item_has_many_normal_albumAlbum ↔ photo membership
itemPhoto item (logical, parent of unit)
unitPhysical file (filename, takentime, folder FK)
folderFolder path
user_infoUser display names

Physical path from DB fields:

  • Personal space: /volume1/homes/<username>/Photos<folder_path>/<filename>
  • Shared space: /volume1/photo<folder_path>/<filename>

TSV columns (10 fields, tab-separated):

1
album_id  album_name  shared  owner  file_owner_id  file_owner  folder_path  filename  takentime  duplicate_hash

Album migration script
#

migrate_albums.py reads the TSV and recreates all albums in Immich:

  1. For each photo, call POST /api/search/metadata with originalFileName
  2. If multiple filename matches, pick closest by takentime (unix timestamp from Synology DB)
  3. Create album via POST /api/albums under the album owner’s account
  4. Add assets via PUT /api/albums/{id}/assets (batches of 100)
  5. Share album via PUT /api/albums/{id}/users with family members (editor role)

Config block at top of script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
IMMICH_URL = "http://<nas-ip>:2283"

API_KEYS = {
    "user1": "<api-key>",   # Get from Immich UI → avatar → Account Settings → API Keys
    "user2": "<api-key>",
}

IMMICH_USER_IDS = {
    "user1": "<immich-uuid>",   # From GET /api/users or Immich admin panel
    "user2": "<immich-uuid>",
}

Usage:

1
2
3
4
5
6
7
# Preview — safe, makes no changes
python3 migrate_albums.py --dry-run
python3 migrate_albums.py --dry-run --album "My Album"

# Run
python3 migrate_albums.py --album "My Album"
python3 migrate_albums.py

Important notes:

  • Script checks for existing album name before creating — safe if re-run, but avoid running twice as it would create duplicates if the check is bypassed
  • Photos with no configured API key are skipped and reported; re-run after adding the key
  • Unmatched photos (not yet uploaded) are printed but don’t block other albums
  • Search is scoped per Immich user, so photo lookup must use the file owner’s API key, not the album owner’s

Verification
#

After migration, verify album counts match between Synology and Immich: query Synology DB for COUNT(*) per album, then compare against GET /api/albums/{id} assetCount field.

Lessons Learned
#

  • Run immich-go on the NAS — no network bottleneck; critical for 100 GB+ libraries
  • Pause Immich ML jobs during upload--pause-immich-jobs=true speeds up import significantly
  • immich-go handles duplicates — safe to re-run; already-uploaded files are detected and skipped
  • Each Immich user needs their own API key — album membership search must use the file owner’s key (search results are user-scoped)
  • Filename + takentime matching is reliableoriginalFileName search plus unix timestamp disambiguation works well for Synology Photos datasets
  • Old psql on Synology — no --csv flag; use -A -F$'\t' -t for TSV
  • --session-tag in immich-go — tags all uploads with a session ID, useful for auditing which files came from which run
  • Album script idempotency — checks existing album names before creating; unmatched photos are non-blocking

Reference
#

  • Immich API docs
  • immich-go docs
  • Full working artifacts (scripts, TSV export, logs): private NAS repo at nas:/volume1/homes/<admin-user>/repos/photo-migration.git