Skip to main content

I Gave My Screenshot Folder a Brain

·1463 words·7 mins
Author
Shane Blaufuss, CISSP

I have a screenshots folder that goes back years. Error messages, memes, half-finished ideas, receipts, a config file I meant to reference later and never did. Hundreds of them, named by whatever ShareX felt like calling them that day, with zero organization. Finding anything meant scrolling. This is the story of turning that folder into something I can actually query in plain English, including searching for the literal text inside the images, and then wiring it up so an AI agent can search it for me.


Key Takeaways
#

  • Immich is a self-hosted photo/video manager with built-in CLIP semantic search and OCR text-in-image search, no manual tagging required
  • Immich’s External Library feature indexes files in place over a network share; it doesn’t need you to upload anything
  • CPU-only ML inference in a VM can silently crash-loop if the virtual CPU type doesn’t expose modern instruction sets, even with no GPU involved at all
  • ImmichMCP exposes Immich’s search API as MCP tools, so an AI agent can query your photo library directly instead of you writing scripts against the API
  • The full stack (Immich + its ML sidecar + an MCP interface) fits in one Docker Compose file

The Setup
#

Proxmox host, already running as a network fileserver via Samba. A Portainer-managed Docker VM for the workloads. A years-old pile of screenshots that lived locally on my laptop and needed a permanent home. The plan: move the screenshots to a network share, point Immich at it as a read-only external library, and get real search out of the result.

Immich was the right call over something like Nextcloud’s Memories app for one reason: it does one thing (photo/video management with real ML search) and does it well, instead of bolting mediocre photo search onto a general-purpose file platform. For a single-purpose problem, a single-purpose tool wins.


The Full Stack
#

Here’s the complete Compose file: Immich server, its Postgres database, Redis, the machine-learning sidecar that does the actual CLIP/OCR inference, and ImmichMCP so an AI agent can query it. Everything on one Docker network so the containers can reach each other by name.

name: immich

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:release
    volumes:
      - ${UPLOAD_LOCATION}:/data
      - /etc/localtime:/etc/localtime:ro
      - ${SCREENSHOTS_LOCATION}:/screenshots:ro
    environment:
      DB_HOSTNAME: database
      DB_USERNAME: ${DB_USERNAME}
      DB_PASSWORD: ${DB_PASSWORD}
      DB_DATABASE_NAME: ${DB_DATABASE_NAME}
      REDIS_HOSTNAME: redis
      UPLOAD_LOCATION: ${UPLOAD_LOCATION}
      TZ: ${TZ}
    ports:
      - '2283:2283'
    depends_on:
      - redis
      - database
    restart: unless-stopped
    healthcheck:
      disable: false

  immich-machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:release
    volumes:
      - model-cache:/cache
    environment:
      TZ: ${TZ}
    restart: unless-stopped

  redis:
    container_name: immich_redis
    image: docker.io/redis:6.2-alpine
    restart: unless-stopped

  database:
    container_name: immich_postgres
    image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.2.0
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_DB: ${DB_DATABASE_NAME}
      POSTGRES_INITDB_ARGS: '--data-checksums'
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped

  immichmcp:
    container_name: immichmcp
    image: ghcr.io/barryw/immichmcp:latest
    environment:
      IMMICH_BASE_URL: http://immich_server:2283
      IMMICH_API_KEY: ${IMMICH_API_KEY}
    ports:
      - '5000:5000'
    restart: unless-stopped
    depends_on:
      - immich-server

volumes:
  model-cache:
  pgdata:

And the matching .env:

UPLOAD_LOCATION=/mnt/immich-upload
SCREENSHOTS_LOCATION=/mnt/your-network-share/Screenshots
DB_USERNAME=postgres
DB_PASSWORD=change-this-to-something-real
DB_DATABASE_NAME=immich
TZ=America/Chicago
IMMICH_API_KEY=fill-this-in-after-first-boot

Two things worth calling out:

  • UPLOAD_LOCATION should be local disk on the Docker host, not the network share. Immich’s internal storage does a lot of small random I/O for thumbnails and encoded video; putting that over CIFS/NFS will make everything feel sluggish.
  • IMMICH_API_KEY doesn’t exist yet on first boot. Bring the stack up once without immichmcp (or let it fail to start; nothing else depends on it), log into Immich, generate a key under Account Settings → API Keys, then fill it in and bring immichmcp up.

Getting the Screenshots Onto the Share
#

If your screenshot tool saves locally, point it at the network share directly instead of copying files over later. On Windows with ShareX, that’s Application Settings → Personal folder path, or per-task under Task Settings → Data → Custom directory. Use the UNC path (\\fileserver\share\Screenshots) rather than a mapped drive letter if there’s any chance the tool runs outside your normal login session; mapped drives don’t always persist.

Once new screenshots are landing on the share, bulk-copy the old backlog over:

robocopy "C:\Users\you\Pictures\Screenshots" "\\fileserver\share\Screenshots" /E /Z /MT:8

/MT:8 parallelizes the copy. Skip this and a few hundred files single-threaded will take a while.


Wiring Up the External Library
#

Immich’s External Libraries feature scans a folder in place; it never needs you to upload anything through the UI. In Immich’s admin panel: Administration → External Libraries → New External Library, then Add folder.

One gotcha here: the path you enter is the path inside the container, not the host path. If your Compose file mounts ${SCREENSHOTS_LOCATION}:/screenshots:ro, the field wants /screenshots, not whatever the host-side path was. Entering the host path gets you Invalid import path: Path does not exist (ENOENT), since the container has no idea that path exists from its point of view.


The CPU Instruction Set Gotcha
#

This is the part I didn’t see coming. I’d initially planned to pass a spare GPU through to the VM for faster ML inference, decided against it (a personal screenshot library doesn’t need that kind of horsepower, and permanently earmarking the host’s only GPU for one VM felt like the wrong tradeoff), and switched the machine-learning container to the plain CPU image.

It deployed fine. The file scan completed, 828 assets showed up in the library. But smart search and OCR search both failed with a generic Failed to search smart error, no useful detail. The Immich server logs told the real story:

Machine learning request to "http://immich-machine-learning:3003" failed: fetch failed

The ML container wasn’t reachable at all. Checking its state directly showed it stuck in a restart loop, unhealthy, and the actual crash log was this:

RuntimeError: NumPy was built with baseline optimizations:
(X86_V2) but your machine doesn't support:
(X86_V2).

Nothing to do with the GPU decision. This was a virtual CPU type problem. The Portainer VM’s CPU type in Proxmox was set to a generic/compatible profile that doesn’t expose modern instruction sets (SSE4.2, POPCNT, etc.) to the guest, even though the physical host CPU supports them fine. NumPy’s prebuilt wheel assumes a baseline that the VM’s virtual CPU was hiding.

The fix: shut the VM down, change Hardware → Processor → Type to host (which passes the physical CPU’s real feature set through to the guest), boot it back up. Clean startup, no crash, Application startup complete.

Worth knowing the tradeoff before you do this: host CPU type means the VM can no longer live-migrate to a different physical host with a different CPU model. For a single-box home lab, that’s not a real cost. For a cluster, it might be.

Also worth knowing: because the container had been crash-looping since the stack was first deployed, the file scan had completed but the ML jobs (smart search embedding, OCR, face detection) never ran at all. Immich doesn’t automatically retro-queue those once the ML service comes back; I had to manually kick them off:

curl -X PUT "http://your-immich-host:2283/api/jobs/smartSearch" \
  -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"command":"start","force":false}'

Same pattern for ocr, faceDetection, and metadataExtraction. force: false only queues assets that never ran the job, which is what you want for a one-time backfill rather than a full reprocess.


Asking an Agent to Search My Screenshots
#

This is the payoff. With ImmichMCP running and registered as an MCP server, an AI agent can call Immich’s search directly, no scripts, no API wrangling on my end mid-conversation.

Two searches, run back to back against the same 828-image library:

Semantic search (“code editor with an error message”) returned a screenshot of a Docker deployment error, correctly matched on meaning, not on any tag or filename.

OCR search (“Deployment error”) returned the exact same screenshot, this time matched on the literal text rendered inside the image. That’s the feature that actually matters for a screenshot folder specifically: most of what’s in there is text, error messages, config snippets, chat logs, and semantic search alone doesn’t reliably surface “the screenshot with this exact string in it.” OCR does.

Live demo: searching a screenshot library with an AI agent via ImmichMCP, using both semantic and OCR search

Between the two, “find the screenshot with that Docker error I hit last week” stopped being a scrolling exercise and became a sentence.


What’s Next
#

Immich doesn’t do automatic object/scene tagging out of the box; there’s an open feature request for it, but it’s not shipped. For now, tags stay manual or scripted. There’s also a rule-based third-party tool, immich-autotag, for config-driven batch tagging if that’s useful before native support lands.

If you’re sitting on your own multi-year screenshot graveyard, this is a weekend project, not a research project. The hardest part won’t be Immich, it’ll be whatever your VM’s CPU type is quietly hiding from you.

If you’re running a home lab and want a second set of eyes on how a piece of it is wired together, whether that’s an indexing pipeline, an MCP integration, or the Proxmox layer underneath it all, that’s exactly the kind of thing worth a conversation before you’re three services deep and debugging a stack trace at midnight.