Skip to main content

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 saysFix
Pinned DINOv2 export missing: ... Run immich-memories models fetchStep 4
Pinned ... export missing: nsfw_marqo has no model: ...Step 4
Output directory is not writable: /app/output: Permission deniedStep 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:

PermissionWhy
Read on assets, people, albums, timeline and searchFinding and reading the pictures
Read on tags, create tags, tag assetsMarking each uploaded film as this app's own, so a later run never films its own render
Upload assets, create and update albumsUpload-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.

Turn on authentication first

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-onStart it withPage
Inference service (heads and detectors on another process or a GPU)docker compose --profile inference up -dInference on a GPU box
Caption server (for the gpu and full tiers)docker compose --profile captioner up -dAdd captions
A readertwo env vars pointing at a model serverAdd a reader
Hardware encoding (Intel Quick Sync, VA-API)the commented devices: blockHardware encoding
Hardware encoding (NVIDIA NVENC)a device reservation and one env varHardware encoding
Render worker (the encode on a GPU box)a second compose file, on that boxRender on a GPU box
Generated music (ACE-Step, MusicGen)a music server of your own, then its URLGenerated 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.