Config file
~/.immich-memories/config.yaml is yours: the app reads it and never writes it, except when you
run immich-memories config move-to-db. Keep it at permissions 600 if it holds API keys. What
you save from the web UI or immich-memories config goes to the database instead (see
where a setting comes from). The annotated example is
examples/config.example.yaml,
and every key with its default is in the config reference. In
Docker you can skip the file entirely and use environment variables.
Quick start config
A full plain-NAS setup. Everything not listed keeps its default: Immich, where home is (which makes trips work and picks your country's public holidays), and where the films go.
immich:
url: "https://photos.example.com"
api_key: "${IMMICH_API_KEY}"
api_version: auto # auto | v2 | v3
trips:
homebase_latitude: 50.85 # without these, no trip is ever a trip
homebase_longitude: 4.35
output:
directory: "~/Videos/Memories"
resolution: "1080p" # 720p, 1080p, 4k
codec: h264 # the default; h265 keeps HDR
hdr_mode: auto # keep HLG/PQ when present, otherwise SDR
Everything else has a default. With codec: h265 and hdr_mode: auto, HLG or PQ footage gives a
10-bit HDR film and SDR clips, photos and titles are converted to the same transfer. H.264 is
always SDR and tone-maps HDR sources.
Trip detection needs both home coordinates. Preflight warns when either is missing or left at
(0, 0), and trips stay off until you set them.
Make it better (optional)
A text model is one block. Leave it out and the app edits on a plain NAS; add it and it writes the titles and picks the music, and with the GPU tier it polishes the cut: What a GPU or a model adds.
llm:
provider: "openai-compatible"
base_url: "http://localhost:8000/v1"
model: "gemma-4-e4b-it-6bit"
What each top-level section is for
Every key and its default is in the config reference. This is the map: what a section decides, and where it is explained.
| Section | What it decides | Explained on |
|---|---|---|
tier | auto (the default), nas, gpu or full: how much preparation runs | Compute tier |
preset | fast fills several output knobs at once (1080p H.264, fast encoder, static titles); your own keys win | Environment variables |
immich | the server, the API key, the API version, and extra accounts | Immich API compatibility, A second Immich account |
trips | where home is (homebase_latitude, homebase_longitude), and how far (50 km), how long (2 days) and how broken (2-day gap) a trip may be | Trip |
defaults | what every film gets unless you say otherwise: blurred backdrop or bars (scale_mode), transitions, the sharing level (family), date and place captions (both on) | Sharing levels, Date and place captions |
output | where films land, resolution, container, codec, HDR, quality | Photos, Live Photos and HDR |
photos | whether photos play (on), for how long (4 s), and when shots are one burst | Photos, Live Photos and HDR |
title_screens | title, month and ending cards, their style, language (locale), and map move lengths | Titles, maps and music |
audio | your own music folder and how generated music is built | Music |
upload | whether a finished film goes back to Immich, into which album | Upload back to Immich |
network | the two outside calls, both off: place names from Nominatim and map tiles | Outside calls |
cache | where previews and downloaded clips live, and how big they may grow | Caches |
database | the store: a SQLite file (default) or PostgreSQL | Database and the store |
render | a GPU box that renders for you | Render on a GPU box |
Under advanced: the ones you are likely to touch are llm (the model reader), auth (sign-in),
automation (the daily film, Automate it), notifications and server
(port, demo mode, the trigger token).
Where a setting comes from
Four sources, strongest first:
- Environment:
IMMICH_MEMORIES_<SECTION>__<FIELD>and the shortcuts in environment variables (IMMICH_URL,IMMICH_API_KEY, ...). config.yaml: this file, which only you write.- Database: what the settings page, Save Config on the Settings page, and
immich-memories config --url/--api-keysaved. One row per key; a key you never saved has no row, so a new default still reaches you after an upgrade. - Default: the value in the config reference.
If a store is configured (a PostgreSQL URL, or a SQLite file that exists) and its settings cannot
be read, the app does not start: the CLI exits with the error and the web UI refuses to start.
The message names the store (password masked) and the cause, such as a refused connection or a
corrupt file. Starting anyway on half the settings could send an automated run somewhere you did
not mean. Fix the database or its URL, or set IMMICH_MEMORIES_SKIP_STORED_SETTINGS=1 to start on
env, config.yaml and defaults only. A SQLite store that does not exist yet is a fresh install and
starts silently.
The first source that sets a key wins, key by key: advanced.llm.model in the file and llm.base_url
in the database work together. The web UI greys out every setting the environment or the file sets
and names the variable or the file key; saving under it would do nothing.
immich-memories config show prints the same report: every key, its value, its source, and the
exact variable or file key that sets it. Secrets print as ***. Give prefixes to narrow it:
immich-memories config show llm immich.url
Moving a key out of the file
Nothing moves from config.yaml into the database on its own; an upgrade leaves your file in charge.
To hand a key to the UI:
immich-memories config move-to-db llm.model automation.cooldown_hours
Keys are runtime paths, without advanced.. Each value is saved to the database, then its line is
removed from the file, wherever it was written (top level or under advanced:). The rest of the file
keeps its values and its ${VAR} references, but not its comments, so the previous file is kept
as config.yaml.bak. A key whose value is a ${VAR} reference is refused: it already comes from
the environment. database.url and database.schema never move, because the app reads them before
the database opens.
Secrets in the database
Keys named api_key, caption_api_key, password, client_secret, trigger_token,
worker_token, token, secret, api_keys or urls (notification URLs carry credentials) are
secrets. In the database they are encrypted with Fernet, under a key derived (HKDF-SHA256) from
IMMICH_MEMORIES_SECRET_KEY. Any string of at least 32 characters works; generate one with
openssl rand -base64 32
and keep it with your other secrets. Without it the UI and the CLI refuse to store a secret and say
so (Settings: "Secrets cannot be saved here until IMMICH_MEMORIES_SECRET_KEY is set"); put the
secret in the environment or config.yaml instead. It is read from the environment only. On
Docker the shipped compose file already passes it through, so set it in .env:
# .env
IMMICH_MEMORIES_SECRET_KEY=paste-the-openssl-output-here
Then docker compose up -d to recreate the container. A key shorter than 32 characters is refused
when you save. Change or lose the key and the
stored secrets stop opening: the app logs which ones and falls back to their defaults, config show
and the settings page mark each one, and you save them again. Logs never print a secret, whichever source it came from.
Compute tier
Leave tier unset, or set tier: auto: the app picks nas, gpu or full from what it finds.
How it decides: The three tiers. Captions from a
vision-capable LLM instead of the caption server are a separate, explicit switch:
LLM captions.
Everyday keys and advanced keys
Everyday sections sit at the top level: immich, defaults, output, audio, title_screens,
cache, database, upload, trips, network, photos, render, title_llm. Tuning sections
go under advanced:: analysis, speech, hardware, llm, musicgen, ace_step, server, auth, automation,
notifications, triage, editorial, inference, free_text. Both placements work and merge key by key at
every depth, and the top-level value wins a tie, so a hand-written
editorial: {preparation: {caption_concurrency: 4}} changes concurrency and keeps the rest of an
advanced.editorial block. The database, config show and config move-to-db use the runtime
path without advanced. (llm.model); the UI and config show name a file key the way you wrote
it (advanced.llm.model). Preparation's former tier override no longer takes precedence
over the product tier.
Unknown keys inside a section are ignored. The keys of the retired per-clip scorer
(content_analysis, audio_content, transcription, description_llm,
analysis.max_refinement_passes, photos.max_ratio and their family), the retired scheduler:
section and a few dials nothing read (cache.max_age_days, title_screens.show_decorative_lines,
triage.enabled, triage.bundle) are dropped by name with a warning, so an old file loads and
tells you what it ignored. Unknown top-level keys and invalid
values (codec: av1) fail with a validation error.
Paths in the config are host paths
Everything else in this file travels to another machine. These keys don't: they name paths on the
machine that wrote them. immich-memories preflight prints one Config paths warning naming every
path that is missing here, so a copied config fails up front instead of hours into a run.
| Key | What it points at |
|---|---|
output.directory | where finished films are written |
cache.directory | previews, thumbnails, downloaded clips |
cache.database | a pre-store cache.db the store imports once; its directory holds the run lock files |
database.url | the store (banked facts and readings, your picture decisions and review edits, people, settings, run history, automation state, special days), when it is a SQLite file (sqlite:///~/.immich-memories/store.db) |
advanced.editorial.annotation_database | deprecated: a legacy annotations.sqlite the store imports once; its directory still holds structure-banks/ (the thumbnail-hash and scene-print caches, and any legacy JSON banks the store imports) |
advanced.triage.encoder | the pinned DINOv2 ONNX export |
advanced.editorial.preparation.head_bundle | a head bundle of your own, for the eight context heads |
advanced.editorial.preparation.marqo_onnx | the pinned sensitive-content export |
advanced.editorial.preparation.detector_cache_dir | the Hugging Face cache the detectors read |
advanced.editorial.preparation.detector_python | an interpreter for the detector worker |
audio.local_music_dir | your own music, read by immich-memories music |
Blank is the default for head_bundle, detector_python and detector_cache_dir, and the portable
value: it means "work it out here". A detector_python that is not on this host (a Mac venv
path carried into a NAS container, or a venv deleted since) stops a cut before it reads a picture,
naming the key; immich-memories preflight shows the same row. Remove the key and the detectors run on the app's own
Python. Containers already pin most of these: the image sets
output.directory to /app/output, and the Kubernetes manifests put the model
paths on the /models claim.
Footage the camera roll did not shoot
Doorbells, screen recorders and messaging apps upload into the same timeline as your phone. Files matching these patterns never reach selection:
advanced:
analysis:
exclude_filename_patterns:
- "RingVideo_*"
- "RPReplay_Final*"
- "Screen Recording *"
- "Screenshot*"
- "img-*-wa[0-9][0-9][0-9][0-9]*"
- "vid-*-wa[0-9][0-9][0-9][0-9]*"
Case-insensitive globs on the original filename. Setting the key replaces the list, so copy the defaults you want to keep.
A still whose EXIF names no camera is dropped too (exclude_stills_without_camera_exif: true, the
default): on iOS a photo saved from a messaging app keeps its IMG_ name and loses only the camera
make. Turn it off if your library is mostly exported or edited originals, which lose the make the
same way. Videos are exempt.
Immich API compatibility
Immich v2 and v3 both work. auto is the default runtime policy: the app detects the server
major and selects the matching API contract. You do not choose a version for each run. Explicit
v2 and v3 values are manual troubleshooting escape hatches for proxies or unusual deployments
that break version detection. An override forces that contract; it is not a normal upgrade step.
Durations, upload fields and search dates are converted for each version, and an unknown major
stops the run with UnsupportedImmichVersion rather than sending requests of the wrong shape.
immich-memories config test
Read-only: it reports the connection and the resolved contract, and does nothing else.
A second Immich account
Two people on one Immich server each upload to their own account. The second one goes under
immich.accounts, by name, next to the primary account (which stays the only upload target):
immich:
url: "https://photos.example.com"
api_key: "${IMMICH_API_KEY}"
accounts:
partner:
url: "https://photos.example.com"
api_key: "${PARTNER_IMMICH_API_KEY}"
Configuring it changes no film on its own. To make one film from both libraries:
immich-memories config test(orpreflight): one line per account, each proving who its key belongs to.- Tell the people registry which face in the partner's account is which person, so
--personfinds them on both sides:immich-memories people bind "Alex" --account partner --id <person-id>(people). immich-memories generate --accounts primary,partner ...reads both into one film. A picture both phones uploaded counts once (Duplicates), and each picture is downloaded through the account that owns it. If that account cannot read it, the run stops and names the account rather than make a film with half a household missing.
Today only generate on the CLI takes --accounts. The web UI, automation, albums and trips read
the primary account, and the film is uploaded to the primary only.
Name rules, secrets and env variables: extra accounts.
Environment variable substitution
These fields expand ${VAR_NAME} at load time:
| Section | Fields |
|---|---|
immich | url, api_key |
llm / title_llm | api_key |
musicgen | base_url, api_key |
ace_step | api_url, api_key |
auth | password, client_secret, issuer_url, client_id |
render | worker_base_url, worker_token |
editorial | annotation_database |
editorial.preparation | head_bundle, detector_python, detector_cache_dir, marqo_onnx, caption_api_key |
Only the braced form expands. A bare $VAR stays as written, because a $ in a password is
ordinary (a warning says so if it matches a variable you have set). For any other field, use
IMMICH_MEMORIES_<SECTION>__<FIELD> (environment variables).
Upload back to Immich
upload:
enabled: true
album_name: "2024 Memories"
Off by default. What Immich sees lists every write it makes.
Outside calls
network:
geocoding: false # nominatim.openstreetmap.org
geocoding_url: "" # your own Nominatim instead, e.g. http://nominatim.lan:8080
map_tiles: false # server.arcgisonline.com
Both off, so a default run reaches your Immich server, the endpoints named elsewhere in this file,
and nothing else. geocoding buys the right district's name where Immich names the neighbouring
town (Wilrijk, not Hoboken) on every name you read, trip names from the map, and place names in the
film's language. It sends rounded coordinates, about a kilometre, once per place; answers are kept
in the store.
geocoding_url points it at a self-hosted Nominatim. map_tiles buys the trip fly-over and the
map behind location cards. Fonts are never fetched at run time (see
fonts). Privacy says exactly what each host receives.
Reader concurrency
Only matters with a reader. advanced.llm.reader_concurrency is unset by default and then read
from llm.base_url: 1 for a loopback or private address or a bare service name, 4 for a public
host. A model on your own machine is one process in front of one accelerator, so four requests
queue there instead of overlapping; a hosted endpoint is a fleet. Set it yourself (1 to 16) for a
local server that does take concurrent requests, or a provider that wants a lower rate.