Docker Compose
The reference install: two files from the repo, two values to fill in, one container. It works on a plain NAS; a GPU or a model makes it better. The shortest path through it is the Quick start; this page is every step with the reasons.
Install
You need Docker Engine with Compose v2 (docker compose version answers), Immich v2 or v3, and
the hardware on Requirements. What the image has been checked on, and when:
Supported and tested.
1. Download the compose file and example.env into an empty directory:
mkdir immich-memories && cd immich-memories
curl -O https://raw.githubusercontent.com/sam-dumont/immich-video-memory-generator/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/sam-dumont/immich-video-memory-generator/main/example.env
cp example.env .env
2. Fill in .env. Two values are required, and the home base makes trips and your country's public holidays work:
IMMICH_URL=http://192.168.1.10:2283 # your Immich, as the container reaches it
IMMICH_API_KEY=your-api-key-here
IMMICH_MEMORIES_TRIPS__HOMEBASE_LATITUDE=50.8503
IMMICH_MEMORIES_TRIPS__HOMEBASE_LONGITUDE=4.3517
TZ=Europe/Brussels
localhost in IMMICH_URL is the container itself, so use the address of your NAS or server.
Every variable .env can hold is on Environment variables. The
compose file also works alone: export IMMICH_URL and IMMICH_API_KEY in your shell instead.
3. Create the output folder and start it:
mkdir -p output
docker compose up -d
The container runs as UID and GID 1000 and writes films to ./output. If Docker creates that
folder for you, root owns it and the container cannot write there. mkdir it first, or fix it in
place with sudo chown -R 1000:1000 output. If your user is not 1000, set user: "<uid>:<gid>" on
the service and chown the config volume to match.
4. Fetch the models, once:
docker compose exec immich-memories immich-memories models fetch
docker compose exec immich-memories immich-memories preflight
models fetch downloads about 140 MB: the pinned DINOv2 encoder behind the eight context heads,
the sensitive-content detector, the document classifier, and the WordNet dictionary a film asked
for in a sentence is read with. All four are checked against a SHA-256 and land on the config volume, so a docker compose pull keeps them. preflight checks
Immich, the model digests, the home base and whether the output folder takes a file.
5. Open http://localhost:8080 and cut a month: Your first film. Then confirm who's who once: Teach it your family.
When a step is missing
A cut (from the web UI, generate or prepare) checks the models and the output folder before it
asks Immich for anything, and refuses to start if one is wrong:
| It says | Fix |
|---|---|
Pinned DINOv2 export missing: ... Run immich-memories models fetch | Step 4 |
Pinned ... export missing: nsfw_marqo has no model: ... | Step 4 |
Output directory is not writable: /app/output: Permission denied | Step 3: sudo chown -R 1000:1000 output |
Home coordinates are not configured (a preflight warning) | Step 2. The cut runs, but no day counts as away from home |
A run with --no-render skips the output check; a dry run skips all of them.
The API key
In Immich: Account Settings > API Keys > New API Key. All works. The minimal key:
| Permission | Why |
|---|---|
| Read on assets, people, albums, timeline and search | Finding and reading the pictures |
| Read on tags, create tags, tag assets | Marking each uploaded film as this app's own, so a later run never films its own render |
| Upload assets, create and update albums | Upload-back to Immich, if you turn it on |
| Delete assets (optional) | Lets upload-back trash the previous render of the same recipe; without it, old copies pile up |
The app never touches your originals. Without the tag permissions the upload still works, the film is not tagged, and on Immich v3 a later run cannot recognise its own render.
Using the CLI
The container has the CLI; the host doesn't. Every immich-memories ... command in these docs
runs as docker compose exec immich-memories immich-memories .... An alias saves typing:
alias im='docker compose exec immich-memories immich-memories'
im generate --memory-type monthly_highlights --year 2025 --month 6
Films into Immich
Every film lands in ./output. To have each generate and daily film uploaded to Immich as well,
into an album (a render from the web UI has its own upload box), add
these to the compose file's environment: block (a line in .env alone does not reach the
container) and run docker compose up -d:
IMMICH_MEMORIES_UPLOAD__ENABLED: "true"
IMMICH_MEMORIES_UPLOAD__ALBUM_NAME: "Memories"
The key needs the upload permissions in the table above. What it writes to Immich: Upload back to Immich.
Reaching the UI from another machine
The compose file publishes 127.0.0.1:8080:8080, so nothing else on your network reaches it.
Authentication is disabled by default and the app holds an API key to your whole library. The port mapping is the only thing keeping it off your network. The UI is single-user, single-replica: keep it at one instance.
Either tunnel (ssh -L 8080:localhost:8080 your-server, then http://localhost:8080 on your
desktop), or do both of these: set IMMICH_MEMORIES_AUTH_USERNAME and
IMMICH_MEMORIES_AUTH_PASSWORD in .env (or OIDC, see Authentication),
then change the mapping to "8080:8080". Port 8080 taken already? Change the left side only:
127.0.0.1:8081:8080.
Next to your Immich stack
Paste the immich-memories service into Immich's own compose file, add
immich-memories-config: to that file's top-level volumes:, set
IMMICH_URL=http://immich-server:2283, and add depends_on: [immich-server]. It then reaches
Immich over the internal network. immich-server listens on 2283 in every Immich v2 and v3
release. An unknown major version stops the run:
Immich API compatibility.
The product tier in compose
The compose file sets IMMICH_MEMORIES_TIER: "auto", so the app picks its tier from what it
finds: a plain NAS until it finds GPU inference. How it decides, and what the gpu tier then needs:
The three tiers.
IMMICH_MEMORIES_EDITORIAL__PREPARATION__DETECTOR_CACHE_DIR puts the document classifier on the
config volume. Keep that line if you write your own service block: without it the classifier lands
in the container's writable layer and the next pull throws it away.
Add-ons, as profiles
Everything optional sits in the same file, off until you ask for it:
| Add-on | Start it with | Page |
|---|---|---|
| Inference service (heads and detectors on another process or a GPU) | docker compose --profile inference up -d | Inference on a GPU box |
Caption server (for the gpu and full tiers) | docker compose --profile captioner up -d | Add captions |
| A reader | two env vars pointing at a model server | Add a reader |
| Hardware encoding (Intel Quick Sync, VA-API) | the commented devices: block | Hardware encoding |
| Hardware encoding (NVIDIA NVENC) | a device reservation and one env var | Hardware encoding |
| Render worker (the encode on a GPU box) | a second compose file, on that box | Render on a GPU box |
| Generated music (ACE-Step, MusicGen) | a music server of your own, then its URL | Generated music |
Banked facts are the same rows whichever process wrote them, so adding or removing an add-on re-derives nothing.
Reaching a model server
Model endpoints must be reachable from inside the container, and localhost there is the
container. For a server on the Docker host itself, the name is host.docker.internal:
IMMICH_MEMORIES_LLM__BASE_URL: "http://host.docker.internal:8000/v1"
IMMICH_MEMORIES_EDITORIAL__PREPARATION__CAPTION_BASE_URL: "http://host.docker.internal:8092/v1"
Docker Desktop resolves it with no setup. On Linux, add this to the service, and have the server
listen on 0.0.0.0 rather than 127.0.0.1 (the container arrives over the bridge):
extra_hosts:
- "host.docker.internal:host-gateway"
Resources
Idle, the container is about 100 MB. Preparation (previews, heads, detectors) wants 2 to 4 GB and
two cores; the render (titles and the FFmpeg encode) wants 4 to 8 GB and four. The compose limit
is memory: 4G: fine for 1080p, give it 8 GB for 4K.
There is no CPU limit, on purpose: cpus: is a CFS quota, and a Synology kernel refuses the whole
up over it. Use cpuset: "0-3" to pin cores instead: On a NAS.
Disk
A film that uploads to Immich does not stay on the output volume: once the upload is confirmed,
the container removes the local file and its run directory, keeping only the run record. On a
node where this container shares a disk with other workloads, that is what keeps a daily cron job
from filling it. If upload_enabled: false, or delivery keeps failing, films pile up on
output.directory the same way they always did: immich-memories runs storage shows what is
using space, and runs delete clears a run's output.
Both the output and cache volumes get a preflight before a run starts and again right before the
film is written: below output.min_free_space_gb (5 GB by default) a run logs a warning naming
the volume, and if the estimated film would not fit at all, it stops before rendering instead of
filling the volume mid-encode. See
health, logs and caches.
Hardening
The compose file carries this block commented out; uncomment it:
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
tmpfs:
- /tmp:size=2G
- /home/immich/.cache:size=1G
A session is a signed cookie; the key that signs it is /home/immich/.immich-memories/.storage_secret
on the config volume (or IMMICH_MEMORIES_STORAGE_SECRET), so logins survive a read-only root and a
restart. For 4K, raise /tmp to 8 GB or drop the entry: FFmpeg's intermediates pass 2 GB.
Daily automation
The container's only process is the web UI, so there is no cron to install. Uncomment these two in
the compose file's environment: block (with TZ set in .env):
IMMICH_MEMORIES_AUTOMATION__ENABLED: "true"
IMMICH_MEMORIES_AUTOMATION__DAILY_AT: "09:00" # read in the TZ zone
Every day at that time the UI process runs what auto run does on the CLI: retry one pending
upload, or make one eligible memory, then notify. A container that was down catches up on start.
Automate it.
The daily film stays in ./output unless upload is on: set IMMICH_MEMORIES_UPLOAD__ENABLED as in
Films into Immich (every generate), or
IMMICH_MEMORIES_AUTOMATION__UPLOAD_TO_IMMICH: "true" (the daily runs only). To fire the same
decision from outside instead (Home Assistant, an Immich workflow, a cron on another box):
Trigger it over HTTP.
Health check and logs
The image's health check hits /health/live. For readiness, use /health/ready: 200 when the
configuration and Immich are usable, 503 otherwise, and it reports the daily automation under
in_process_scheduler (with login on, only to a signed-in session). /health always answers
200 and is not a probe.
docker inspect --format='{{.State.Health.Status}}' immich-memories
docker compose logs -f immich-memories # the UI and the daily timer
A cut's own output is kept per job under cache/web-jobs/, a daily run's under
cache/automation-output/, both on the config volume.
Log level, JSON lines and a log file: Health, logs and caches.
What to keep
/home/immich/.immich-memories/store.db is the expensive file: every fact, caption and reading
the editor banked, every picture you cleared or ruled out, your people, run history, automation
state and the special-days catalogue. Lose it and the next cut reads the library again. A cache.db
beside it is a leftover from before the store: nothing writes it, and once imported it can go. The
store sits on the config volume, so moving host means copying that volume (moving an install).
Back the store up without stopping anything:
docker compose exec immich-memories immich-memories store backup
It lands in /home/immich/.immich-memories/backups/ with a manifest beside it. The image ships the
PostgreSQL client tools, so the same command works when the store is on PostgreSQL
(backup and restore). To copy the backups off the volume:
docker compose cp immich-memories:/home/immich/.immich-memories/backups ./backups. A restore
needs the app stopped: Restore in a container.
The previews and clips a cut downloads sit on the same volume, under cache/: up to 10 GB of each
by default, and safe to delete. Sizes and caps: Caches.
The store: SQLite or PostgreSQL
The compose file defaults to a SQLite file on the config volume, one host, one writer, which is
right for the single container this file runs. The commented postgres service and
IMMICH_MEMORIES_DATABASE_URL line switch the store to PostgreSQL instead: a separate service, a
separate database on your own PostgreSQL instance, or a dedicated schema inside an instance you
already run (Immich's, for example). See Database and the store for the four
modes and the SQL for the dedicated-schema one.
Updating
docker compose pull
docker compose up -d
docker compose exec immich-memories immich-memories models fetch
up does not re-pull a latest the machine already has, so pull comes first. models fetch is a
no-op when the files are right, and downloads again when a release moves a pin. Config, banks and
films live on the volume and the bind mount, so a recreate loses nothing. Take a store backup
first if you might go back: Rollback.
Custom music
Upload a track in a cut's Render panel takes a track from the browser. For CLI runs,
bind-mount a directory and pass --music /app/music/track.mp3.
Building the image
docker build --build-arg APP_VERSION=0.0.0 --build-arg INSTALL_EXTRAS=all -f docker/Dockerfile .
Needs BuildKit (the default since Docker 23). From a checkout, make docker fills in the version
and git metadata; INSTALL_EXTRAS=none make docker builds the slim image, which cannot run the
heads.