OpenFactory Immich photo vault with Immich server, ML worker, Postgres pgvector and Redis queue

Build a Self-Hosted Immich Photo Vault on OpenFactory

A four-VM Immich lab: app + ML worker + Postgres-pgvector + Redis, from one prompt

May 7, 2026

← Back to Blog

Immich is the self-hosted answer to Google Photos. It does the things you actually use the cloud for — automatic mobile backup, face recognition, smart search, shared albums — and keeps the library on hardware you own. It's the second-most-deployed app in the 2026 r/selfhosted survey, and the most-planned-next.

This post walks through building the Immich stack on OpenFactory: four buildable VMs — the Immich app server, the ML worker for embeddings and face recognition, Postgres with pgvector for vector search, and a Redis queue — all generated from a single prompt and shipped as bootable ISOs.

What you'll build

  • immich-server (10.71.0.10:2283) — the upload + library API the mobile and web clients talk to.
  • immich-ml (10.71.0.20:3003) — embedding and face-recognition worker, split out so it can scale (or run on a GPU box) independently.
  • postgres-pgvector (10.71.0.30:5432) — Postgres pre-configured for Immich plus a sample embeddings (asset_id UUID, embedding VECTOR(512)) schema and the bootstrap SQL for the vectors extension.
  • redis (10.71.0.40:6379) — queue for upload, transcoding, and ML pipeline jobs.

Why build it on OpenFactory

  • The ISO is the spec. The env file that points the server at the ML worker, Postgres, and Redis is baked into the image. No docker-compose.yml in a dotfiles repo somewhere.
  • Pgvector wired in. The schema and extension are bootstrapped on first boot. Real Immich runs the same migrations on top.
  • ML splits cleanly. The ML worker is a separate VM so you can run it on a GPU host while the app and database stay on a mini-PC.
  • Scenario assertions ride along. The build fails closed if pgvector isn't reachable from the server, if the ML stub isn't up, or if the API ping breaks.

Topology

Four Debian Trixie VMs on 10.71.0.0/24. The Immich server talks to all three backends; the ML worker also writes embeddings back into Postgres.

The prompt

Paste this verbatim into the chat builder at console.openfactory.tech. Nothing above or below it — the builder expects the prompt body to start at the “Build a compact multi-node lab…” line.

Build a compact multi-node lab named `immich-photo-vault`.

Output discipline: keep the plan small. Use one startup script per node, about 25 shell lines or less. Do not install the real Immich server, machine-learning model weights, PyTorch/TensorFlow, libvips, ffmpeg, pgvector binaries, or external `apt` repos. Do not pull GB-scale ML assets at build time. Write deployment-time config examples and tiny Python stdlib or shell compatibility stubs only. The goal is a buildable preparation lab, not a production deployment.

## Topology

Create 4 buildable `debian-trixie` nodes, all `x86_64`, SSH enabled, DHCP/default route intact with lab aliases, firewall disabled, DNS `1.1.1.1` and `8.8.8.8`, user `ops` password `immich-ops` in `sudo`. Every recipe must set top-level `test_config` to `{ "enabled": false, "tests": [] }`.

- `immich-server`: role `app`, 4 GB RAM, 24 GB disk, alias `10.71.0.10/24`, x `230`, y `60`
- `immich-ml`: role `ml-worker`, 4 GB RAM, 24 GB disk, alias `10.71.0.20/24`, x `110`, y `220`
- `postgres-pgvector`: role `database`, 2 GB RAM, 16 GB disk, alias `10.71.0.30/24`, x `350`, y `220`
- `redis`: role `queue`, 1 GB RAM, 8 GB disk, alias `10.71.0.40/24`, x `230`, y `380`

Connections: `immich-server` to `postgres-pgvector:5432`, `redis:6379`, `immich-ml:3003`; `immich-ml` to `postgres-pgvector:5432` as embeddings-write intent.

## Common Recipe Requirements

All nodes: features `headless`, `ssh`; packages `openssh-server`, `python3`, `curl`, `jq`, `iproute2`, `netcat-openbsd`, `ca-certificates`. Each startup script adds the alias with `IFACE=$(ip route show default | awk '{print $5; exit}')`, `ip link set "$IFACE" up || true`, and `ip addr add <alias> dev "$IFACE" || true`. If `os.startup_scripts[].after` is present, it must be the string `"network-online.target"`, not an array.

## Node Requirements

`immich-server`: features `headless`, `ssh`. Write `/etc/immich/server.env` with `IMMICH_PORT=2283`, `DB_HOSTNAME=10.71.0.30`, `DB_PORT=5432`, `DB_USERNAME=immich`, `DB_DATABASE_NAME=immich`, `REDIS_HOSTNAME=10.71.0.40`, `IMMICH_MACHINE_LEARNING_URL=http://10.71.0.20:3003`. Create `/var/lib/immich/{upload,library,thumbs}` mode `0750 ops:ops`. Add a Python stdlib service on `0.0.0.0:2283` exposing:
- `GET /api/server-info/ping` -> `200 {"res":"pong"}`
- `GET /api/server-info/version` -> `200 {"major":1,"minor":0,"patch":0,"flavor":"compat"}`
- `GET /metrics` -> `immich_server_compat_up 1`
Register `immich-server-compat.service`.

`immich-ml`: features `headless`, `ssh`, `python`. Write `/etc/immich/ml.env` with `MACHINE_LEARNING_PORT=3003`, `MACHINE_LEARNING_CACHE_FOLDER=/var/lib/immich-ml/cache`. Create `/var/lib/immich-ml/{cache,models}` mode `0750 ops:ops`. Add a Python stdlib service on `0.0.0.0:3003` exposing `GET /ping` -> `200 {"status":"ok"}`, `POST /predict` -> `200 {"prediction":"compat-stub","confidence":0.0}` for any JSON body, and `GET /metrics` with `immich_ml_compat_up 1`. Register `immich-ml-compat.service`. Write `/root/immich-ml-notes.md` warning that real ML inference requires CLIP/face-recognition model weights and CUDA or CPU-vectorized ONNX runtime at deployment time.

`postgres-pgvector`: features `headless`, `ssh`, `postgresql`; packages `postgresql`, `postgresql-client`. Configure Postgres to `listen_addresses = '*'` on port `5432`, best-effort create role/database `immich` password `immich` with `host immich immich 10.71.0.0/24 md5` in `pg_hba.conf`. Write `/root/immich-pgvector-bootstrap.sql` containing `CREATE EXTENSION IF NOT EXISTS vectors;` and a sample `embeddings (id BIGSERIAL PRIMARY KEY, asset_id UUID, embedding VECTOR(512))` schema. Expose a tiny `:9187/metrics` listener with `pg_compat_up 1`.

`redis`: features `headless`, `ssh`, `redis`; packages `redis-server`, `redis-tools`. Enable `redis-server`, bind to `127.0.0.1` plus `10.71.0.40`. Add a Python stdlib `:9121/metrics` listener with `redis_compat_up 1`.

## Scenario

Emit exactly one group scenario named `immich-photo-vault-validation`. Put `custom_tests[].assertions[]` inside the scenario entry; leave `scenarios[].tests` empty. Every assertion needs `on_vm`. Use only `port_listening`, `command_output`, and `http_responds`; do not emit `vm_boots`, `network_reachable`, or `service_running`.

- `Stack ports listen`: `port_listening` for `immich-server:2283`, `immich-ml:3003`, `postgres-pgvector:5432`, `postgres-pgvector:9187`, `redis:6379`, `redis:9121`.
- `Server pings`: on `immich-server`, `curl -fsS http://localhost:2283/api/server-info/ping | jq -e '.res == "pong"' >/dev/null && echo server-ok`.
- `ML pings`: on `immich-ml`, `curl -fsS http://localhost:3003/ping | jq -e '.status == "ok"' >/dev/null && echo ml-ok`.
- `Server reaches backends`: on `immich-server`, `nc -z -w 5 10.71.0.30 5432 && nc -z -w 5 10.71.0.40 6379 && nc -z -w 5 10.71.0.20 3003 && echo backends-reachable`.
- `Postgres bootstrap SQL present`: on `postgres-pgvector`, `grep -q 'CREATE EXTENSION' /root/immich-pgvector-bootstrap.sql && echo bootstrap-ready`.

Preserve warnings that real Immich server binary distribution, ML model weight provisioning (CLIP + face recognition), pgvector / `vectors` extension installation, mobile-app auth tokens, S3 / object storage backends, library import scheduling, thumbnail generation, library backup, and `10.71.0.0/24` aliasing are deployment-time concerns.

Running it

  1. Open the chat builder at console.openfactory.tech and paste the prompt into a new conversation.
  2. Review the streamed build plan. You'll see the topology, per-node recipes, and the scenario assertions that will run after boot. Edit the prompt and re-run if anything is off.
  3. Click Build group. OpenFactory fans the plan out to per-node ISO builds. When every ISO reaches built, boot the group on the runner network from the same UI.
  4. Exercise the stack. The scenario assertions run automatically against the live VMs. From the host you can also hit the service ports directly to confirm end-to-end behavior.

Driving OpenFactory from an AI agent instead of the browser? The same flow is exposed through the OpenFactory MCP server — submit the prompt programmatically, get the build-plan preview back, and call create_build / start_vm on the resulting recipes. Single-image builds go straight through the openfactory CLI.

What's still your responsibility

The prompt produces a buildable preparation lab — the right topology, the right ports listening, deployment-time config templates dropped in the right places, and tiny compatibility services that prove the wiring works. A few things still sit outside the recipe and need operator attention before this carries real load:

  • Real Immich binaries. Replace the stdlib compatibility services with the real Node.js Immich server and ML container (upstream releases on ghcr).
  • ML model weights. CLIP, face recognition, and the OCR models need to ship to the ML worker before search and tagging start producing results.
  • pgvector / vectors extension. Install the actual extension on top of the schema that's already in place.
  • Mobile-app authentication. JWT secret rotation, push notification keys, and the public hostname the apps connect to.
  • Backups. /var/lib/immich/upload and the Postgres volume both need off-host snapshots; losing either loses the library.
  • Object storage. S3-compatible external storage (MinIO, R2, Wasabi) for the original files if the upload volume outgrows local disk.

Where to go next

Photos are step one; documents are step two. The Paperless-ngx document lab gives you the same shape for paper. For verifying the kernel and userspace under the stack, see the runtime attestation post. And the Enterprise & GxP page covers fleet-scale rollouts.

Ready to ship this in production?

OpenFactory's free flow is for browsing. Persistent VMs, SSH access, snapshots, your own ISO, and fleet deployment live on a paid plan.