11 — Immich Photo Storage¶
Self-hosted photo and video library running on the Z2, replacing iCloud Photos as the primary archive. Tailscale-only; deployed as a Docker Compose stack; historical iCloud library bulk-imported via icloudpd + immich-go.
Behind the Caddy reverse proxy (May 2026). Immich's container binds
127.0.0.1:2283only and is reached athttps://immich.z2mini.gabrielgabrie.com(auto-renewing Let's Encrypt cert via Caddy). The iOS app's Server Endpoint URL =https://immich.z2mini.gabrielgabrie.com/api. On the box itself:http://127.0.0.1:2283. The oldhttp://z2mini:2283/http://100.67.235.68:2283direct URLs no longer work — apps aren't reachable on the tailnet directly anymore, only through Caddy.Migrating from iCloud? The full migration runbook is the last section of this page — see Migrating an iCloud Photos library.
Overview¶
The Immich stack is four containers managed by Docker Compose:
| Container | Image | Purpose |
|---|---|---|
immich_server |
ghcr.io/immich-app/immich-server |
Web UI, REST API, background microservices |
immich_machine_learning |
ghcr.io/immich-app/immich-machine-learning:*-cuda |
Face recognition, smart search, OCR — GPU-accelerated on the Quadro T2000 |
immich_postgres |
ghcr.io/immich-app/postgres (vectorchord-enabled) |
Main DB + vector embeddings |
immich_redis |
docker.io/valkey/valkey:9 |
Job queue + cache |
Two related tools, each with its own page-section below:
icloudpd— separate Docker container at/data/docker/icloudpd/. One-shot CLI for downloading from iCloud Photos. Not a long-running service.immich-go— single Go binary at/home/gabriel/scripts/immich-go. Used for bulk-importing files into Immich via its API.
Reachable at https://immich.z2mini.gabrielgabrie.com from any tailnet device. Not exposed to the public internet.
Design decisions¶
These were considered carefully — reverting any of them needs a real reason.
| Decision | Reasoning |
|---|---|
| Immich over PhotoPrism, Ente, Photoview | Best iPhone integration, on-server AI/ML, active project momentum (May 2026) |
Bound to 127.0.0.1:2283, fronted by Caddy |
Apps live behind the single Caddy ingress at https://immich.z2mini.gabrielgabrie.com; never reachable on the tailnet directly or publicly. (Was 100.67.235.68:2283 before the Caddy migration.) See 17-caddy.md. |
| Storage Template Engine ON | Library on disk organized by capture date (library/admin/2026/2026-05-03/IMG_*.HEIC); recoverable and browsable even without the DB |
| Bind-mount ML cache instead of Docker named volume | Keeps everything Immich-related under /data/docker/immich/; ML weights (~2-3 GB) live on /data rather than the OS drive |
| GPU acceleration enabled (May 2026) | Quadro T2000 with NVIDIA driver 595 + nvidia-container-toolkit. Pre-signed linux-modules-nvidia-595-generic package bypasses DKMS so no MOK enrollment is needed under Secure Boot. ML container uses the :cuda image variant with extends: hwaccel.ml.yml service: cuda. Peak VRAM ~3.5 / 4 GB with CLIP + face + OCR all loaded; processing the post-ingest ML wave on GPU is roughly 4-5× faster than the CPU baseline |
restart: unless-stopped (not always) |
Survives reboots, but docker compose stop actually stops |
| iOS auto-backup OFF during bulk migration, ON afterwards | iOS only sees photos local to the device — wrong tool for bulk. After migration completes (and after pruning iCloud to a small "keep" set so re-uploads don't undo Immich cleanup), auto-backup becomes the daily-driver: every new iPhone capture pushes to Immich automatically |
| iCloud Photos cleared post-migration; iCloud subscription downgraded to 200 GB tier ($3.99/mo) | Originally the plan was to keep iCloud Photos full as an off-site safety net until a 2 TB external SSD arrived. Decided instead to clear iCloud Photos at the same time as the migration verify, accepting that Immich on /data is the only photo copy until the 2 TB drive arrives. The 50 GB iCloud tier was evaluated and rejected because iMessage (~60 GB after attachment purge) plus the Family Sharing pool (~85 GB) don't fit, and the iPhone (128 GB) can't hold the full Messages history locally either |
Evaluated and not chosen: PhotoPrism, Ente, Photoview (all viable; Immich's iPhone story tipped it).
Install¶
Directory layout¶
/data/docker/immich/
├── docker-compose.yml ← stack definition, version-pinned
├── .env ← DB password (mode 600), version, paths
├── library/ ← photos + thumbnails (bind-mounted to container)
├── postgres/ ← Postgres data (mode 700, uid 999) — NEVER rsync
└── model-cache/ ← ML model weights
docker-compose.yml¶
The compose file mirrors the upstream Immich v2.7.5 template verbatim except for two intentional customizations:
- Port published on
127.0.0.1only ("127.0.0.1:2283:2283") — Immich is fronted by Caddy athttps://immich.z2mini.gabrielgabrie.com, not reachable on the tailnet directly (see 17-caddy.md) - ML cache is a bind mount (
${MODEL_CACHE_LOCATION}:/cache) instead of a Docker named volume
name: immich
services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
volumes:
- ${UPLOAD_LOCATION}:/data
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
- "127.0.0.1:2283:2283"
depends_on:
- redis
- database
restart: unless-stopped
healthcheck:
disable: false
immich-machine-learning:
container_name: immich_machine_learning
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
volumes:
- ${MODEL_CACHE_LOCATION}:/cache
env_file:
- .env
restart: unless-stopped
healthcheck:
disable: false
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
healthcheck:
test: redis-cli ping || exit 1
restart: unless-stopped
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23
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
shm_size: 128mb
restart: unless-stopped
healthcheck:
disable: false
Important: do not add a
command:override to thedatabase:service. The postgres image already configuresshared_preload_libraries=vchord.soitself; an override clobbers that and immich-server crash-loops onCREATE EXTENSION vchord.
.env¶
# Path config
UPLOAD_LOCATION=/data/docker/immich/library
DB_DATA_LOCATION=/data/docker/immich/postgres
MODEL_CACHE_LOCATION=/data/docker/immich/model-cache
# Version pin — verify current stable:
# curl -s https://api.github.com/repos/immich-app/immich/releases/latest | grep tag_name
IMMICH_VERSION=v2.7.5
# Timezone
TZ=America/Toronto
# Database
DB_PASSWORD=<48-char hex string from `openssl rand -hex 24`>
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
Generate the DB password directly into the file rather than typing it:
DB_PASS=$(openssl rand -hex 24)
# ... write .env using $DB_PASS in the heredoc, then:
chmod 600 /data/docker/immich/.env
First boot¶
cd /data/docker/immich
docker compose config --quiet # validate YAML + variable resolution
docker compose pull # ~6 GB across four images
docker compose up -d
Then poll for health (postgres initdb ~30 s, immich-server migrations ~30-60 s):
When all four containers report (healthy), the stack is ready. Quick HTTP check:
First-run admin account¶
Open https://immich.z2mini.gabrielgabrie.com in your browser, set the admin email and password (separate from any other credentials — Immich has its own user DB).
Decisions to make on the first-run wizard:
- Storage Template Engine: ON. Files get organized by capture date on disk. Flipping this later moves every file — set it before importing anything substantial.
- Google Cast: OFF. Only useful with a Cast device on the same LAN. Toggle on later if needed.
Operations¶
Start / stop / pull updates¶
cd /data/docker/immich
docker compose up -d # start (or recreate after pull)
docker compose stop # stop without removing
docker compose down # stop and remove containers (data preserved)
# Updates: bump IMMICH_VERSION in .env, then:
docker compose pull
docker compose up -d
Logs¶
docker compose logs -f --tail 50 immich-server
docker compose logs -f --tail 50 immich-machine-learning
docker compose logs -f --tail 50 database
Disk usage¶
du -sh /data/docker/immich/library/ \
/data/docker/immich/postgres/ \
/data/docker/immich/model-cache/
Permission denied on
postgres/is expected — it's mode 700 owned by uid 999 (the postgres container user). Usesudo duor run from inside the container if you need to peek.
Total asset count via API¶
curl -sH "x-api-key: $(cat ~/.immich-api-key)" \
http://127.0.0.1:2283/api/server/statistics | python3 -m json.tool
API keys¶
Account avatar (top right) → Account Settings → API Keys → New API Key. The key is shown once — save it immediately:
umask 077 && read -s -p "Paste API key: " K \
&& printf '%s\n' "$K" > ~/.immich-api-key \
&& unset K \
&& ls -la ~/.immich-api-key
The file ends up at mode 600 because of the umask 077.
Connecting from on the server itself¶
Immich's container binds 127.0.0.1:2283, so scripts/tools running on z2mini use http://127.0.0.1:2283 (or http://localhost:2283). https://immich.z2mini.gabrielgabrie.com also works from the box (resolves to 100.67.235.68, goes through Caddy). What no longer works: http://z2mini:2283 and http://100.67.235.68:2283 — those bindings were removed when Immich moved behind Caddy. (Pre-Caddy, the opposite was true — Immich bound the tailnet IP and localhost didn't work; that's flipped now.)
iOS app setup¶
- Confirm Tailscale on the iPhone is connected (Tailscale iOS app → toggle should say "Connected").
- Smoke-test reachability in Safari first:
https://immich.z2mini.gabrielgabrie.comshould load the login page. - Open the Immich app → Log In (or whatever the entry button is in your app version).
- Server Endpoint URL:
https://immich.z2mini.gabrielgabrie.com/api - Sign in with your admin email and password.
- Photos permission: Allow Access to All Photos — not "Selected Photos." Selected mode hides the rest of your library from the app.
- Don't enable auto-backup yet if you're about to do a bulk migration — see the next section.
Migrating an iCloud Photos library¶
The bulk migration uses two independent tools chained together:
iCloud → icloudpd container → /data/icloud-import/YYYY/MM/DD/ → immich-go → Immich library
(transient staging, (storage template applies)
deleted after verify)
Why not just use the iOS app's auto-backup? iOS only sees photos local to the device. With "Optimize iPhone Storage" on (the default), the bulk of any sizeable library lives in iCloud, not on the phone — so iOS auto-backup would only migrate a small subset. The bulk migration has to come from iCloud's web API directly.
Pre-flight: iCloud account requirements¶
icloudpd authenticates against Apple's iCloud Photos API. Two account settings must be true:
- Advanced Data Protection: OFF. With ADP enabled, icloudpd hard-fails with
ACCESS_DENIED. If you turned ADP off specifically for the migration, you can re-enable it once the bulk download is done. If it was already off, this is a non-issue. - "Access iCloud Data on the Web": ON. Settings → [Your Name] → iCloud → scroll down.
icloudpd setup¶
Layout:
/data/docker/icloudpd/
├── docker-compose.yml ← image pin + bind mounts
└── cookies/ ← Apple session token (root-owned, ~2 month validity)
docker-compose.yml:
name: icloudpd
services:
icloudpd:
image: icloudpd/icloudpd:1.32.2
container_name: icloudpd
volumes:
- /data/docker/icloudpd/cookies:/cookies
- /data/icloud-import:/data
environment:
- TZ=America/Toronto
There's no up -d for icloudpd. It's invoked on demand with docker compose run --rm for each session.
First run — interactive auth + smoke test¶
The first invocation is interactive: Apple ID password and 2FA code. The cookie this seeds is then valid for ~2 months.
cd /data/docker/icloudpd
docker compose run --rm icloudpd icloudpd \
--directory /data \
--username your-apple-id@example.com \
--cookie-directory /cookies \
--recent 5 \
--size original \
--live-photo-size original
--recent 5 downloads only the 5 most recent items — fast smoke test that validates auth, cookie persistence, AND the file landing path (/data/icloud-import/YYYY/MM/DD/).
Subsequent runs reuse the cookie automatically — no auth prompts unless Apple invalidates the session.
Bulk download with watch-mode auto-retry¶
Apple's iCloud Photos API intermittently throttles or disconnects (icloudpd issue #1308). When this happens mid-download, icloudpd exits cleanly with INFO Cannot connect to Apple iCloud service and exit code 0 — which is not the same as "completed."
The mitigation is icloudpd's --watch-with-interval 60 flag — runs in an infinite loop with a 60-second sleep between passes. Each new pass picks up exactly where the previous one left off. Once the library is fully downloaded, subsequent passes are quick no-ops.
Wrapped in a script at /home/gabriel/scripts/icloudpd-bulk-download.sh:
#!/bin/bash
set -o pipefail
BULK_LOG=/tmp/icloudpd-bulk.log
echo "" | tee -a "$BULK_LOG"
echo "================================================================" | tee -a "$BULK_LOG"
echo " iCloud bulk download (watch mode) started: $(date '+%Y-%m-%d %H:%M:%S %Z')" | tee -a "$BULK_LOG"
echo "================================================================" | tee -a "$BULK_LOG"
cd /data/docker/icloudpd
docker compose run --rm icloudpd icloudpd \
--directory /data \
--username your-apple-id@example.com \
--cookie-directory /cookies \
--size original \
--live-photo-size original \
--set-exif-datetime \
--watch-with-interval 60 \
--no-progress-bar 2>&1 | tee -a "$BULK_LOG"
Launch in a detached tmux session so it survives ssh disconnects:
Monitor:
Reattach (read-only-ish — careful not to type into the session):
Detach again with Ctrl+B then D.
For a 31k-item / ~458 GB library, expect 2-5 days at Apple's API throttle.
Stop the watch loop when done¶
--watch-with-interval never exits on its own. Decide the bulk is complete when:
- File count under
/data/icloud-import/matches your iCloud library size and stops growing across multiple watch cycles, AND - The bulk log shows nothing but
already existslines for several minutes
Then kill the session:
Mind the disk during the ingest — both copies live on the same filesystem¶
Lesson from this homelab's first migration (May 2026): during ingest, the photo bytes exist twice on
/data— once as the icloudpd staging tree (/data/icloud-import/, 458 GB) and once as Immich's managed copy under/data/docker/immich/library/(also ~458 GB plus thumbnails). On the Z2 Mini's 938 GB/datafilesystem this fits, barely, but at the 86%-uploaded mark/datahit 100% and Postgres started failing every write withcould not extend file ... No space left on device— surfacing as 500 Internal Server Errors on every immich-go upload, eventually triggering postgres + immich-server container restarts.Mitigations to apply BEFORE running the bulk ingest: - Confirm
df -h /datashows enough free space for the combined staging + library + thumbnails footprint, not just one copy. Aim for at least 1.3× the source library size in free space. - If staging would exceed available headroom, stage on a different filesystem (e.g., the planned/mnt/scratchpartition once the OS-drive shrink is done — see 10-system-reference.md). - Or delete-as-you-go: after each immich-go batch (e.g., per year), parse~/.cache/immich-go/*.logforINF (uploaded successfully|server has duplicate)lines andrmthe corresponding files from staging. Reclaims space incrementally. - During long migrations, watchdf -h /datalike you'd watch GPU during ML — Immich's job queues will keep filling and ML keeps trying to process even with the disk full, masking the underlying capacity issue.If you do hit the disk-full wall: the recovery is to parse immich-go's logs for confirmed-successful uploads, build a delete list,
sudo rmthose files (they're root-owned because icloudpd runs as root in its container), then re-run immich-go on whatever's left in staging. See the troubleshooting section below for the exact pattern.
Ingesting into Immich with immich-go¶
Once the bulk download is done, ingest the staging directory into Immich. Use immich-go upload from-folder — not from-icloud. The from-icloud subcommand is for Apple's iCloud-takeout zip files (the Privacy Portal export), not icloudpd's output.
Generate an Immich admin API key (Account Settings → API Keys → New) and save it to ~/.immich-api-key mode 600 as shown earlier.
Recommended flags for resilience (learned from the May 2026 migration):
--on-errors continue— don't bail on the first transient 500. The defaultstoppolicy will abort the whole run if any single upload fails; withcontinue, immich-go logs the error and moves on, and you can retry just the failed files in a later pass.--concurrent-tasks 4— lower than the default 20. Reduces server-side load on Postgres + ML and tends to fail less under contention.--pause-immich-jobs=false— by default, immich-go pauses Immich's background jobs during the upload to avoid contention. For long ingests, leave them running so the ML / thumbnail / metadata queues drain in parallel with the upload phase.
Always dry-run first:
/home/gabriel/scripts/immich-go upload from-folder \
--server http://127.0.0.1:2283 \
--api-key "$(cat ~/.immich-api-key)" \
--manage-heic-jpeg StackCoverJPG \
--no-ui \
--dry-run \
/data/icloud-import
The dry-run reports counts: how many would upload, how many are already in Immich (deduplicated by content hash), how many errors. If clean, drop --dry-run and run for real.
Verify and clean up¶
After immich-go reports success:
- Check the Immich web UI count matches expectations —
find /data/icloud-import -type f | wc -lshould roughly equaltotal assetsin Immich's API stats. - Spot-check a handful of photos in the timeline — thumbnails generated, faces detected, smart search returning sensible results.
- Then delete the staging directory (it's served its purpose):
Post-migration¶
- If you turned off Advanced Data Protection on iCloud specifically for icloudpd auth, re-enable it now. Skip this step if it was already off.
- Enable iOS Immich app auto-backup for ongoing iPhone captures (Settings → Backup).
- Consider keeping the icloudpd compose around as an incremental sync mechanism (
--until-found Nflag) — useful as a "iCloud → Immich one-way mirror" if you keep iCloud Photos partially active. - Clear iCloud Photos and downgrade your iCloud subscription. Don't assume the 50 GB tier is the right target — what your iCloud actually holds besides Photos (iMessage history + attachments, Family Sharing pool, device backups, iCloud Drive) often dwarfs 50 GB. Check Settings → [Your Name] → iCloud → Manage Account Storage to see the breakdown before picking a tier. For Gabriel: post-migration usage was ~150 GB (iMessage ~60 GB even after aggressive attachment pruning, Family Sharing pool ~85 GB, iPhone Backup ~20 GB), so the 200 GB tier ($3.99/mo) was the right landing point.
- Be deliberate about the off-site safety net. Immich now has an on-site backup (
/mnt/backup/current/immich/+ the.sql.gzDB dumps — see Backup considerations below), so a deleted-from-Immich or drive-failure scenario is covered. But the photo library is not on the off-site drive (~520 GB > the 500 GB off-site T5), so a fire/theft scenario that takes out both the apartment's drives still loses the photos. Closing that gap needs a >1 TB off-site drive — until then it's a known, accepted risk. (iCloud Photos was cleared at the May 2026 downgrade — see Decisions table — so it's not the safety net anymore either.)
Backup considerations¶
Immich IS in the nightly backup (since May 2026)¶
Once the backup drive was upgraded to a 1 TB SSD, Immich was wired into ~/scripts/backup-files.sh (see 05-backups.md). Two pieces:
1. The photo files — the entire UPLOAD_LOCATION (/data/docker/immich/library/) is rsync-mirrored to /mnt/backup/current/immich/ every night at 03:00, with --delete. That covers library/ (~254 GB original photos), upload/ (~188 GB app uploads), encoded-video/ (~68 GB transcodes — included, deliberately), thumbs/ (~7.6 GB), backups/ (Immich's own DB dumps — see below), profile/. The rsync uses --no-owner --no-group because library/ files are owned by Immich's container uid, not gabriel — on restore you re-apply ownership or let Immich fix it.
2. The PostgreSQL database — via Immich's own built-in nightly auto-backup, NOT a filesystem copy. The postgres/ data dir is owned by the container's uid and a live Postgres data dir rsync'd as a snapshot is corrupt — so it is never filesystem-rsync'd. Instead, Immich's own database auto-backup (Admin → Settings → Backup Settings) runs at 02:00, keeps the last 14, and writes compressed dumps named immich-db-backup-YYYYMMDDTHHMMSS-v<ver>-pg<pgver>.sql.gz (~137 MB each) into library/backups/. The 03:00 current/immich/ rsync then picks those up — so they land at /mnt/backup/current/immich/backups/. The chain is: Immich dumps → /data/docker/immich/library/backups/ (02:00) → backup-files.sh rsyncs that into /mnt/backup/current/immich/backups/ (03:00).
The Immich "Database Backups" setting must stay enabled. It's ON by default — leave it on. If you ever turn it off, the photo files keep being backed up but the database stops being captured, a silent gap.
What's NOT backed up¶
/data/docker/immich/postgres/— covered by the.sql.gzdumps instead (above). Never filesystem-rsync a live Postgres data dir./data/docker/immich/model-cache/— ML model weights (~2-3 GB); re-downloaded automatically by the ML container, no need to back it up.
Restore¶
Follow Immich's official backup/restore docs (immich.app/docs/administration/backup-and-restore):
- Restore the photo files:
rsync -a /mnt/backup/current/immich/ /data/docker/immich/library/(the.sql.gzdumps come along insidelibrary/backups/). Re-apply ownership if Immich complains. - Restore the config (
.env,docker-compose.yml,docker-compose.yml.pre-cuda,hwaccel.ml.yml) — these are in/mnt/backup/current/service-config/immich/. - Bring up the stack, but recreate the Postgres database from the most recent
.sql.gzdump before startingimmich-server: recreate the DB, create the required extensions, load the dump (per the Immich docs above).model-cache/rebuilds itself on first ML run.
See 08-recovery.md → Step 6b for the full disaster-recovery walkthrough.
Off-site: the photo library is not on the off-site drive — at ~520 GB it doesn't fit on the 500 GB off-site T5 (
backup-offsite.shcovers documents, music, the DB dumps, and every service's config, but skipscurrent/immich/). So the photos have copies only at the apartment (/datalive +/mnt/backupbackup) — a known, accepted gap until a >1 TB off-site drive exists. See 05-backups.md.
Troubleshooting¶
"vchord must be loaded via shared_preload_libraries" — immich-server in crash loop:
- Cause: a
command:override on thedatabase:service is replacing the postgres image's built-inshared_preload_libraries=vchord.sosetting. - Fix: remove the
command:directive from thedatabase:service in compose. Theghcr.io/immich-app/postgresimage configures itself; do not override.
icloudpd exits cleanly after a few minutes with "Cannot connect to Apple iCloud service":
- Cause: Apple's API throttling or transient disconnect (icloudpd #1308).
- Fix: use
--watch-with-interval 60(already baked intoicloudpd-bulk-download.sh). Subsequent passes resume from where the previous left off.
icloudpd fails immediately with ACCESS_DENIED:
- Cause: Advanced Data Protection enabled on the Apple ID.
- Fix: Settings → [Your Name] → iCloud → disable Advanced Data Protection. If you turned it off only for the migration, re-enable it once the bulk download completes.
immich-go reports duplicates for photos that look different in the iOS app:
- Cause: content-hash dedup is by exact bytes. iOS share-sheet uploads convert HEIC → JPG; icloudpd downloads originals. Same visual photo, different bytes, not deduplicated.
- Fix: post-hoc stack with
immich-go stackif duplicate UI entries are bothering you. Otherwise leave them.
Storage Template doesn't reorganize files:
- Cause: Storage Template wasn't enabled on first-run, OR you uploaded files via External Library mode (which doesn't move files).
- Fix: Administration → Settings → Storage Template → enable, then run the storage-template-migration job.
A script on z2mini can't reach Immich (or it uses an old http://z2mini:2283 URL):
- Cause: Immich now binds
127.0.0.1:2283only (it moved behind Caddy) —http://z2mini:2283andhttp://100.67.235.68:2283no longer exist. - Fix: use
http://127.0.0.1:2283(orhttp://localhost:2283), orhttps://immich.z2mini.gabrielgabrie.com(via Caddy).
Mid-migration: every immich-go upload starts returning 500 Internal Server Error after some hours of successful progress:
- Cause:
/datais full. Postgres can't write (could not extend file ... No space left on device), so every API call that creates a DB row fails. immich-server log will show repeatedRequest abortedwarnings andPostgresError: ... No space left on device. - Fix:
- Stop the immich-go run:
pkill -f "immich-go.*upload from-folder" - Identify staging files that are confirmed in Immich already (and therefore safe to delete) by parsing immich-go's log:
- Delete those (they're root-owned because icloudpd runs as root in its container):
- Verify
df -h /datarecovered to a healthy level. - Postgres will recover on its own (it may have been container-restarted by Docker's healthcheck during the no-space window — also normal). immich-server restarts shortly after.
- Re-run immich-go with
--on-errors continue --concurrent-tasks 4 --pause-immich-jobs=falseto pick up whatever's left.
Pull is slow / fails on Wi-Fi:
- Cause: typical residential Wi-Fi is the bottleneck for the 6 GB initial pull.
- Fix:
docker compose pullis resumable — re-running picks up partially-downloaded layers. If Apple's iCloud throttle kicks in during a migration, the watch loop handles it; for the initial Immich pull, just retry.
See also¶
- 05-backups.md — the nightly backup; Immich's
library/is mirrored to/mnt/backup/current/immich/and its DB is captured via thelibrary/backups/dump chain - 08-recovery.md — disaster recovery, including the per-service restore (Step 6b)
- 10-system-reference.md — quick lookup for paths, ports, services
- z2mini-context-for-ai.md — the AI-context document, kept in sync with this page