Skip to main content

NAS-Only Setup (Docker)

For Synology, QNAP, Unraid, and TrueNAS users running Immich on the same NAS or local network. Docker-only, no LLM, no AI music, CPU encoding.

Who this is for

You have a NAS with Docker support (Synology DSM 7+, Unraid, TrueNAS SCALE, QNAP Container Station). You're already running Immich there. You want memory videos without setting up Python environments or GPU passthrough.

Architecture

┌─────────────────────────────────────────┐
│ NAS (Synology/Unraid/TrueNAS) │
│ │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ Immich │ │ Immich Memories │ │
│ │ (port 2283) │←─│ (port 8080) │ │
│ │ │ │ CPU encoding │ │
│ │ │ │ PIL titles │ │
│ └─────────────┘ └──────────────────┘ │
│ │
│ Volumes: config, output, video cache │
└─────────────────────────────────────────┘

NAS setup diagram

Docker Compose

services:
immich-memories:
image: ghcr.io/sam-dumont/immich-video-memory-generator:latest
container_name: immich-memories
ports:
- "127.0.0.1:8080:8080" # loopback only — see "Reaching the UI" below
volumes:
- immich-memories-config:/home/immich/.immich-memories
- ./output:/app/output # create it first and chown to the container UID, see below
environment:
IMMICH_URL: "${IMMICH_URL}"
IMMICH_API_KEY: "${IMMICH_API_KEY}"
restart: unless-stopped
deploy:
resources:
limits:
memory: 4G
cpus: "4"

volumes:
immich-memories-config:

Reaching the UI

The mapping above is loopback-only, so on a headless NAS nothing reaches the UI until you do one of two things. The cheap one is an SSH tunnel — ssh -L 8080:localhost:8080 your-nas, then open http://localhost:8080 on your desktop. Nothing is published and there is nothing to secure.

To publish it on the LAN instead, do both halves: turn on authentication first — the app holds an Immich API key to your whole photo library — then change the mapping to "8080:8080".

One more thing to get right before the first run:

  • Ownership of ./output: the image already writes to /app/output, so the mount above is where the videos land. The container runs as UID/GID 1000. Create the folder yourself (mkdir -p output) so Docker doesn't create it as root; if your NAS user isn't 1000, chown 1000:1000 output. On Synology/QNAP where that is awkward, use a named volume (immich-memories-output:/app/output) and docker cp the finished video out — or turn on upload-back to Immich and fetch it there.

.env file

IMMICH_URL=http://immich-server:2283
IMMICH_API_KEY=your-api-key-here

If Immich runs on the same Docker network, use the container name (immich-server). If it's on a different machine or behind a reverse proxy, use the full URL (https://photos.example.com).

What works

  • Clip scoring: motion analysis, face detection (CPU-based), favorites boost, audio signals
  • Title screens: PIL-based renderer (works everywhere, no GPU needed)
  • Custom music: upload your own MP3/WAV in Step 3
  • All memory types: year in review, monthly, person spotlight, trips (if GPS data exists)
  • Scheduling: set IMMICH_MEMORIES_AUTOMATION__ENABLED=true and IMMICH_MEMORIES_AUTOMATION__DAILY_AT=09:00 (plus TZ) in the compose environment: — the UI process runs the daily decision itself, so there is no cron to install. immich-memories auto install is for host installs and cannot write a cron job inside the container; if you would rather drive it from the NAS host's scheduler, use docker exec immich-memories immich-memories auto run --quiet --cooldown 24 and leave the built-in timer off. See Daily automation
  • Photo support: Ken Burns animations, face-aware pan, blur backgrounds

What still curates without an LLM

Worth being precise about, because "no LLM" reads like "no curation" and that is not what happens. Only the final review is gated on a model — the rest of the loop is arithmetic over data Immich and the analyzer already produced:

  • The verify pass. A clip can reach the cut carrying a metadata guess for a score rather than a real one. Verify finds those, analyses them properly, and re-runs selection. This runs regardless of whether an LLM is configured.
  • The mechanical judge. A non-favourite scoring below the floor never ships, and the memory cannot end on a clip that is both its weakest and well under the average — a video should not end on its worst shot. Pure thresholds, no model.
  • Event-vs-catalogue detection. A dense day is only promoted as an event if Immich recognised people in enough of it. This is what stops 130 photos of an empty apartment (a property viewing) from beating the month's real days. It reads Immich's existing face recognition, so it costs nothing extra.
  • Favourites first. Every favourite in range is taken before anything competes on score, and favourites are exempt from the judge's floor.
  • Adaptive coverage. Every month or week in range is guaranteed at least one clip, and a sole representative of a period is protected when the cut is scaled down to fit.

What you actually give up is redundancy detection across the finished cut — two near-identical moments can both survive, because nothing read their descriptions side by side.

What doesn't work

  • LLM content analysis: needs a separate LLM server (mlx-vlm, Ollama, vLLM). Without it the pipeline loses the holistic review — the one pass that reads every clip's description together and spots the same birthday candles twice. Everything else in the curation loop still runs; see below.
  • AI music generation: MusicGen and ACE-Step need GPU servers. Use custom music upload instead.
  • GPU encoding: NAS CPUs (Celeron, Atom, low-end Xeon) don't have usable GPU encoders. Encoding is CPU-only via libx264.
  • Taichi GPU title renderer: falls back to PIL. Title screens still look good, just without particle effects and animated gradients.

One switch: preset: fast

Add IMMICH_MEMORIES_PRESET=fast to the compose environment: (or preset: fast at the top of config.yaml) and the CPU-only profile is on: 1080p H.264 with the fast encoder preset and medium quality, static title backgrounds instead of animated ones, no per-clip speech analysis, photos capped at a quarter of the cut, three refinement passes instead of ten, and analysis depth auto running as fast (favorites first). Every value you set explicitly still wins, and the web UI's Step 3 shows a banner when the preset is active. immich-memories --preset fast generate … does the same for one CLI run.

max_refinement_passes is a bill, not just a clock

Selection verifies, judges and reviews in a loop, up to analysis.max_refinement_passes times (default 10). Three loops share that budget, and each round that changes the cut sends the descriptions back to the model. On a NAS that is time. If llm.base_url points at a hosted API rather than a box you own, it is money, and it is the largest single multiplier on what a run costs you.

preset: fast sets it to 3. Set it yourself to override that either way:

advanced:
analysis:
max_refinement_passes: 3 # or 1 to stop after the first pass

or per run: immich-memories generate --refinement-passes 3 …

Lower is cheaper and faster. The cost of lowering it is that a clip admitted by the last refill may ship with less scrutiny than the ones before it.

    environment:
IMMICH_URL: "${IMMICH_URL}"
IMMICH_API_KEY: "${IMMICH_API_KEY}"
IMMICH_MEMORIES_PRESET: "fast"

Performance expectations

One measured number (2026-08-18), so you can calibrate: a monthly memory from a real library, 14 clips (7 videos + 6 photos, HDR iPhone sources), 62 s of 1080p H.264 out, cold cache, the Docker image with --cpus=4 --memory=4g and no GPU (4 cores of an Apple M5 Max running the linux/arm64 image):

ProfileWall timeAnalysisRenderOutput
preset: fast10 min 08 s7.4 min2.7 min30 MB
default15 min 42 s10.1 min5.6 min87 MB

Analysis (downloading, downscaling, scoring every candidate clip) is where the time goes, not encoding, and it is cached: a second run of the same month reuses the scores and takes roughly a third as long. A Celeron-class NAS core is a good deal slower than an M5 core, so budget 2–3× these numbers there.

If the render column is what you want to shrink, start with the title screens rather than the encoder: see title rendering is the bottleneck.

Memory usage peaks at about 2-3 GB during encoding. The 4 GB limit in the compose file gives enough headroom. If you're encoding 4K (not recommended on NAS hardware), bump it to 8 GB.

The streaming assembler keeps memory constant regardless of clip count: it processes one clip at a time instead of loading everything into RAM.

Tips for NAS users

  • Synology: use Container Manager (formerly Docker). Create the project from the compose file above.
  • Unraid: add as a Docker container in the Unraid UI or use Docker Compose Manager plugin.
  • TrueNAS SCALE: use the built-in Apps system or deploy via custom Docker compose.
  • QNAP: use Container Station with the compose file.
  • Keep the video cache enabled (default). It caches downloaded Immich clips locally, so repeat runs skip the download phase. Default cache limit: 10 GB, evicts files older than 7 days.