Install with Docker
No Python environment to manage. Pull the image, set two env vars, done.
Quick start
Create a .env file next to your docker-compose.yml:
IMMICH_URL=https://photos.example.com
IMMICH_API_KEY=your-api-key-here
Get the API key from Immich: Account Settings > API Keys > New API Key. When Immich asks which permissions to grant, pick All — or, for a minimal key: read access to assets, people, albums, timeline and search, plus asset upload and album create/update if you turn on upload-back to Immich. This tool never deletes or modifies existing assets.
Then grab the compose file from the repo and start it:
curl -O https://raw.githubusercontent.com/sam-dumont/immich-video-memory-generator/main/docker-compose.yml
docker compose up -d
UI is at http://localhost:8080. The compose file publishes the port as
127.0.0.1:8080:8080, so nothing else on your network reaches it out of the box.
Authentication is disabled by default. Inside the container the app listens on 0.0.0.0 — the
compose port mapping is the only thing keeping it off your network, and the app holds an Immich
API key to your whole photo library. The UI is single-user, single-replica; keep this service at
one instance.
To reach the UI from another machine, make both changes together: enable
authentication, then swap the mapping to - "8080:8080" and
run docker compose up -d again. On a headless box you can skip the exposure entirely and
tunnel instead: ssh -L 8080:localhost:8080 your-server.
The compose volume at /home/immich/.immich-memories must stay writable. It holds config, cache,
automation history, and pending-delivery state.
The image writes to /app/output (the Dockerfile sets IMMICH_MEMORIES_OUTPUT__DIRECTORY) and the
compose file mounts ./output there, so videos land on your host without extra configuration. That
is an environment variable, so it beats output.directory in config.yaml — to write somewhere
else, override the variable in the service's environment: block, not in the YAML.
What you do have to get right is ownership. The container runs as the unprivileged immich user, UID/GID 1000 — the first user on most
Linux hosts, so a ./output folder you create yourself is writable without any chown. If Docker
creates the folder for you it is owned by root; then either mkdir -p output before the first
up, or sudo chown 1000:1000 output. On a host where your user is not 1000, set
user: "<uid>:<gid>" on the service — the config volume must then be writable by that UID too
(bind-mount it and chown it the same way) — or use a named volume
(immich-memories-output:/app/output) and copy files out with docker cp.
Resource requirements
The container's resource usage depends on what phase it's in:
| Phase | RAM | CPU | When |
|---|---|---|---|
| Idle (UI running, waiting) | ~100 MB | minimal | Most of the time |
| Analysis (downloading + scoring clips) | 2-4 GB | 2+ cores | First run or new videos, and where most of the wall time goes |
| Assembly (title screens + FFmpeg encode) | 4-8 GB | 4+ cores | Final video generation |
The quickstart compose file sets memory: 4G and cpus: 4. That's fine for 1080p. For 4K output, bump to 8 GB.
Inside assembly, the title screens cost more than the encode does on a CPU-only box: measured at --cpus=2, title rendering was ~263 s of a ~339 s assembly. See CPU-Only Mode before you size a box around the encoder.
Temporary files during encoding can use 2x the size of your source clips. A 10-minute memory from 50 clips might need 5-10 GB of temp space.
Standalone Docker run
If you don't use compose:
docker run -d \
--name immich-memories \
-p 127.0.0.1:8080:8080 \
-e IMMICH_URL=https://photos.example.com \
-e IMMICH_API_KEY=your-api-key-here \
-v immich-memories-config:/home/immich/.immich-memories \
-v ./output:/app/output \
ghcr.io/sam-dumont/immich-video-memory-generator:latest
Building the image yourself
Published images include the all dependency set. For a local build, make the extras explicit:
docker build --build-arg APP_VERSION=0.0.0 --build-arg INSTALL_EXTRAS=all -f docker/Dockerfile .
From a checkout, make docker runs the same build with the version and git metadata filled in
(INSTALL_EXTRAS=none make docker for a slim image), and make docker-run starts it.
INSTALL_EXTRAS is validated at build time; use an explicit supported extra set rather than
assuming a base image happens to include optional features.
Adding to your existing Immich stack
Drop this into your Immich docker-compose.yml. It connects directly to Immich's internal network: no need to expose Immich externally.
services:
immich-memories:
image: ghcr.io/sam-dumont/immich-video-memory-generator:latest
ports:
- "127.0.0.1:8080:8080" # drop the `127.0.0.1:` only after enabling auth
environment:
- IMMICH_URL=http://immich-server:2283
- IMMICH_API_KEY=${IMMICH_API_KEY}
volumes:
- immich-memories-config:/home/immich/.immich-memories
- ./output:/app/output # pre-create and chown to the container UID, see above
networks:
- default
depends_on:
- immich-server
volumes:
immich-memories-config:
Inside Immich's own compose stack, immich-server listens on 2283 (every Immich v2/v3 release). This tool works with Immich v2 or v3 — not "or newer": an unrecognised major stops the run. See Immich API compatibility. If you're connecting from a separate compose stack, use the URL you open Immich with in your browser instead (for example http://nas.local:2283).
Environment variables
| Variable | Required | Description |
|---|---|---|
IMMICH_URL | Yes | Your Immich server URL |
IMMICH_API_KEY | Yes | Immich API key |
IMMICH_MEMORIES_PRESET | No | fast = CPU-only/NAS profile (1080p h264, fast encoder, static titles, no speech pass, favorites-first analysis). Explicit settings win. See the NAS guide. |
IMMICH_MEMORIES_OUTPUT__DIRECTORY | No | Already /app/output in the image. Set it only to write somewhere else — and note it beats output.directory in config.yaml. |
IMMICH_MEMORIES_STORAGE_SECRET | No | Session secret for the web UI. Auto-generated into the config volume if not set, so sessions already survive a restart. Set it explicitly to share one secret across hosts. It does not make multiple replicas supported. |
IMMICH_MEMORIES_LLM__BASE_URL | No | LLM endpoint (any OpenAI-compatible API). On its own it does nothing for scoring — see the next row. |
IMMICH_MEMORIES_LLM__MODEL | No | Model name as the server reports it. Tested against Qwen3.6-27B and Qwen3.6-35B-A3B (e.g. qwen3.6:27b on Ollama). |
IMMICH_MEMORIES_CONTENT_ANALYSIS__ENABLED | No | true to actually use the LLM for clip scoring. Off by default. |
IMMICH_MEMORIES_AUTH_USERNAME | No | Basic auth username. Set with IMMICH_MEMORIES_AUTH_PASSWORD to enable auth. |
IMMICH_MEMORIES_AUTH_PASSWORD | No | Basic auth password. Set with IMMICH_MEMORIES_AUTH_USERNAME to enable auth. |
IMMICH_MEMORIES_AUTOMATION__ENABLED | No | true to run the daily auto run decision inside the container. Off by default. See Daily automation. |
IMMICH_MEMORIES_AUTOMATION__DAILY_AT | No | Wall-clock time for that run, HH:MM in the container's TZ (default 09:00). |
All config options can also be set via env vars with the IMMICH_MEMORIES_ prefix. Double underscores for nesting: IMMICH_MEMORIES_ANALYSIS__SCENE_THRESHOLD=25.
Security hardening
The quickstart compose is intentionally minimal. For production use, add these options:
services:
immich-memories:
# ... your existing config ...
# Prevent privilege escalation
security_opt:
- no-new-privileges:true
# Drop all Linux capabilities
cap_drop:
- ALL
# Read-only root filesystem (writes go to tmpfs and volumes)
read_only: true
tmpfs:
- /tmp:size=2G
- /home/immich/.cache:size=1G
deploy:
resources:
limits:
memory: 8G
cpus: "4"
The root docker-compose.yml has these options as a commented section: uncomment to enable.
Nothing else needs a writable root. The web UI keeps its session storage under
/home/immich/.immich-memories/.nicegui on the config volume (the image sets
NICEGUI_STORAGE_PATH), so logins survive both a read-only root and a restart.
Don't repoint that variable at /tmp or another tmpfs — sessions would work
until the next restart and then quietly log everyone out.
The default tmpfs is 2 GB. If you're generating 4K videos, FFmpeg intermediates can exceed that. Either increase to 8 GB (/tmp:size=8G) or remove the tmpfs entry and let the container write to disk.
Daily automation
The container's only process is the web UI, so there is no cron to install. Turn on the built-in timer instead:
environment:
- IMMICH_MEMORIES_AUTOMATION__ENABLED=true
- IMMICH_MEMORIES_AUTOMATION__DAILY_AT=09:00
- TZ=Europe/Brussels # daily_at is read in this zone
Every day at that time the UI process runs the same auto run decision as the CLI (retry one
pending upload, or generate one eligible memory, then notify) with the same lock and history. If
the container was down at that time it catches up on start; if the day's run already happened it
waits for tomorrow. Details and the config-file form: automated generation.
Check it from outside with /health/ready — the in_process_scheduler block shows next_run,
running, and the last outcome. Automation history stays in the config volume, so keep it
persistent (see below).
Health check
The Dockerfile health check hits /health/live, which reports only that the web process is alive.
It works with Docker's native health reporting and monitoring tools like Uptime Kuma. Use
/health/ready for dependency readiness; it returns 200 only when configuration and Immich are
usable, otherwise 503. /health always returns HTTP 200 and rewrites only a ready payload to
ok; it is a compatibility endpoint, not the readiness status endpoint.
# Check health status
docker inspect --format='{{.State.Health.Status}}' immich-memories
/health/live returns 200 with status: alive; /health/ready returns the detailed status and
the readiness code above.
Cache persistence
Analysis scores are cached in ~/.immich-memories/cache.db (SQLite). This avoids re-running LLM analysis on every generation. The config volume already covers it:
volumes:
- immich-memories-config:/home/immich/.immich-memories # includes cache.db
To back up or migrate the cache separately:
# Backup
docker exec immich-memories immich-memories cache backup /app/output/cache-backup.db
# Export to JSON (portable)
docker exec immich-memories immich-memories cache export /app/output/scores.json
# Import on a new instance
docker exec immich-memories immich-memories cache import /app/output/scores.json
# Check what's cached
docker exec immich-memories immich-memories cache stats
Export to JSON before migrating. The JSON format is portable across SQLite versions and architectures. The binary backup is faster but ties you to the same SQLite version.
Custom music
In the web UI, pick Upload file in Step 3 and the browser uploads the track — no mount needed.
For CLI runs inside the container, bind-mount a directory and point --music at it:
volumes:
- ./music:/app/music:ro
docker exec immich-memories immich-memories generate --year 2024 --music /app/music/track.mp3
Updating
docker compose pull
docker compose up -d
Your config and output videos are in named volumes / bind mounts, so nothing is lost on container recreation.