- Rust 69.1%
- TypeScript 21.6%
- Lua 3.9%
- JavaScript 3.7%
- Shell 1.4%
- Other 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .cargo | ||
| .forgejo/workflows | ||
| crates | ||
| deploy | ||
| docker | ||
| docs | ||
| proxmox | ||
| web | ||
| .env.example | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
| rustfmt.toml | ||
Imora
A clean room reimplementation of the Immich and Noodle Gallery server in Rust, with a new web app. Imora speaks the same HTTP API and runs on an existing Immich database, so it can take over from a running instance.
Install
Imora is two programs: imora, the server with the web app built in, and imora-ml, the machine learning
service. Both run on Linux, amd64 or arm64. They need PostgreSQL with pgvector or VectorChord, Redis or Valkey,
and for the server libvips, ffmpeg and ExifTool 13.59. On an empty database Imora creates the schema itself.
Docker
docker/compose.yml has the shape of Immich's compose file, with Imora's images:
mkdir imora && cd imora
curl -fsSLO https://code.vexcited.com/imora/imora/raw/branch/main/docker/compose.yml
curl -fsSL -o .env https://code.vexcited.com/imora/imora/raw/branch/main/docker/example.env
# set UPLOAD_LOCATION, DB_DATA_LOCATION and DB_PASSWORD in .env
docker compose up -d
The images are code.vexcited.com/imora/imora and code.vexcited.com/imora/imora-ml, tagged with the release
version and latest. On amd64 the machine learning image also comes as -cuda and -openvino,
docker/hwaccel.ml.yml and docker/hwaccel.transcoding.yml pass the devices through.
Proxmox VE
Run on the Proxmox host, it creates a Debian 13 container with everything in it:
bash -c "$(curl -fsSL https://code.vexcited.com/imora/imora/raw/branch/main/proxmox/ct/imora.sh)"
Inside the container, update installs the latest release. proxmox/install/imora-install.sh also installs
Imora on any Debian 13 machine.
Binaries
Every release carries imora-<version>-linux-<amd64|arm64>.tar.gz with both binaries. The systemd units are
the ones proxmox/install/imora-install.sh writes.
Migrating from Immich or Noodle Gallery
Imora works on the database and the files Noodle Gallery 5.7.0 leaves, and never migrates the schema. Once it runs, going back is a matter of starting Gallery again on the same database and files.
- Noodle Gallery 5.7.0 switches directly.
- Immich up to 3.2 first starts Noodle Gallery 5.7.0 once, which migrates the database to its schema, then switches. Immich 3.3 and later add migrations Gallery 5.7.0 does not know, it cannot take them over yet.
Make a database dump before anything, Imora has no backup feature:
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres | gzip > dump.sql.gz
On its first start Imora renames what Immich named after itself: the database function immich_uuid_v7
becomes imora_uuid_v7, with the triggers that call it, the .immich markers of the storage folders become
.imora, and the queues move from immich_bull to imora_bull in redis. Gallery and Immich do not start on that
database afterwards without renaming them back, and their mobile apps do not connect to Imora.
Environment variables
Immich's and Gallery's IMMICH_* variables are named IMORA_* in Imora, which warns about any IMMICH_* one
left at startup. DB_*, REDIS_* and MACHINE_LEARNING_* keep their names. The machine learning service listens on
IMORA_ML_HOST and IMORA_ML_PORT where Immich's read IMMICH_HOST and IMMICH_PORT.
sed -i 's/^IMMICH_/IMORA_/' .env
Docker
- From Immich up to 3.2: replace the server image with
ghcr.io/open-noodle/gallery-server:v5.7.0, rundocker compose up -d, wait forImmich Server is listeningindocker compose logs immich-server, thendocker compose down. - In
docker-compose.yml, replace the server image withcode.vexcited.com/imora/imora:<version>and the machine learning one withcode.vexcited.com/imora/imora-ml:<version>, adding-cudaor-openvinowhen the old one had it. Keep the volumes, the database and redis services as they are: the media stays mounted at/data, where the database's paths point. Rename theimmich-machine-learningservice toimora-machine-learning, the url Imora's settings default to, or set that url in the admin settings. - Rename the variables in
.envas above. In the machine learning service,IMMICH_PORTbecomesIMORA_ML_PORT. docker compose up -d.
Installs from before Immich 1.137 mount the media at /usr/src/app/upload. Keep that mount, or mount it at
/data: Imora rewrites the stored paths on its first start, like Immich does when its media folder moves.
Proxmox and bare metal
For the community-scripts Immich LXC, a Gallery install, or any install with systemd units:
- Stop and disable the old services,
systemctl disable --now immich-web immich-ml. - Run
proxmox/install/imora-install.shwithIMORA_EXISTING=1, it installs Imora next to the old install and leaves the database and media alone. - Write
/opt/imora/.envfrom the old.env, renamed as above, withDB_*pointing at the existing database andIMORA_MEDIA_LOCATIONat the existing media folder. Moving the media is fine: pointIMORA_MEDIA_LOCATIONat the new folder and Imora rewrites the stored paths on its first start. systemctl enable --now imora imora-ml.- Once it works, remove the old install. Keep the database dump until you are sure.
Layout
| path | what |
|---|---|
crates/ |
the server, a Cargo workspace of the crates below: Tokio, axum, and tokio-postgres on the Immich schema |
web/ |
the web app: Solid 2, @solidjs/router, @solidjs/meta, UnoCSS, unplugin-icons, Vite, Bun |
deploy/ |
the security headers, and a script putting a build of this checkout on production |
docker/ |
Dockerfiles of the server and machine learning images, compose file |
proxmox/ |
Proxmox VE container script and the Debian installer it runs |
.forgejo/ |
release workflow |
Crates
The server is split into crates so a change rebuilds only what depends on it, and the crates at the same level compile in parallel. The graph is wide and shallow: pure libraries at the bottom, one vocabulary crate, feature crates side by side on top of it, then the api and the binary.
| crate | what |
|---|---|
imora-js |
javascript semantics gallery's output depends on: Number#toString, Number(), JSON.stringify, Date, lodash and luxon |
imora-html-to-text |
html-to-text 9.0.5 with htmlparser2's tokenizer, the plain text of emails |
imora-emails |
gallery's react email templates, filled like react renders them |
imora-user-agent |
ua-parser-js 2.0.10 with javascript regex semantics, for session device names |
imora-bullmq |
a bullmq 5.81.3 client on gallery's queues in redis |
imora-media |
mime tables, libvips and sharp's pipeline, ffmpeg and ffprobe, exiftool, thumbhash |
imora-ml |
gallery's machine learning service on onnx runtime, and the imora-ml binary that serves it |
imora-core |
config, errors, database, request validation, auth and access, system config, jobs and the worker loop, the socket, AppState |
imora-people |
people and faces, suggestions, face repair |
imora-assets |
asset details and updates, files, metadata, edits, duplicates, stacks, downloads, hls |
imora-sharing |
albums, activities, shared links, notifications |
imora-sync |
the mobile apps' sync streams |
imora-admin |
user administration, server statistics, storage migration, integrity checks |
imora-upload |
uploads, with multer and busboy's multipart handling |
imora-library |
timeline, search, map, memories |
imora-jobs |
gallery's job handlers and what they hand over to |
imora-api |
the http routes and the web app |
imora |
the binary: the server, the machine learning service and the admin commands |
Implemented so far
-
password login, logout, token validation, change password
-
OAuth / OpenID Connect: authorize with PKCE, callback, account link and unlink, mobile redirect, back-channel logout, RP initiated logout with
id_token_hint -
sessions (cookies, bearer,
x-imora-*headers), child sessions, PIN code and locked sessions -
API keys with permission checks
-
/server/{ping,version,config,features,about},/users/meand/users/:id/profile-image, OAuth logins sync the storage quota and label claims on every login like Immich 3.3, and import the providerpictureclaim as a thumbnail in the configured format when the user has no profile picture -
timeline:
/timeline/{buckets,bucket,bucket-covers}with every filter of gallery'sTimeBucketDtoexcept shared spaces and ocr, which fail with a 500 instead of answering differently, and/assets/:id/thumbnail. The web app shows the whole library as a justified grid by month and day with a date scrubber on the right. -
albums: every
/albumsroute (list with filters, statistics, names, create, update, delete, map markers, adding and removing assets, sharing with roles, leaving),/activitiescomments and likes, andGET /usersfor sharing. Invites and new photos go through gallery's notification jobs (updates after gallery's five minute delay), which write the in app notification and send the email. The web app has an albums page, album pages with sharing, activity, cover and sort order, and adding photos from the timeline selection. -
shared links: every
/shared-linksroute (album and individual links, passwords with gallery's cookie, custom slugs, expiry, hidden metadata, adding and removing assets), and shared link credentials on the album, timeline and thumbnail routes. Links created from a shared space fail with a 500./share/:keyand/s/:slugcarry gallery's open graph tags. The web app has a link dialog on albums and the photos selection, a shared links page and the public share page. -
downloads:
/download/infoand/download/archive(stored zips streamed as files are read, form posts included) and/assets/:id/original. Albums, selections and shared links can be downloaded from the web app. -
uploads:
POST /assets(multer and busboy ported, so malformed forms, field order, latin1 file names and every error answer like gallery), thex-imora-checksumand constraint duplicate paths, quota, live photo links, shared link uploads,POST /assets/bulk-upload-check,GET /server/media-typesandPOST/DELETE /users/profile-imagethrough the same libvips pipeline as gallery's. The web app uploads from the header button, drag and drop (folders too) or paste, hashes each file in a worker to skip duplicates, shows gallery's upload panel, uploads into albums from their page and lets shared link visitors upload when the link allows it. Profile pictures are set from account settings. -
asset viewer:
GET/PUT/PATCH /assets/:id, bulkPUT/PATCH/DELETE /assets,/assets/:id/ocr,/assets/:id/video/playback,/trash/restore/assetsand/stacks/:id. The web app opens photos and videos from the photos, album, person and share pages at<page>/photos/:idwith zoom, pinch, swipes, keyboard shortcuts, live photos, stacks, the ocr overlay and a details panel where owners edit the description and date. Owners can favorite, archive, trash, share and add to albums from the viewer, album members can set the cover or remove the photo. -
people: every
/peopleroute (the identity based list, search, statistics, one person, their faces and face crops, create, update, hide, delete, merge, same person merges, separating a profile, representative faces, reassigning),/faces(list, draw, reassign, remove), face suggestions andGET /search/person, plus person filters on the timeline. Shared space profiles and merges that would combine another user's people fail with a 500. The web app has a people page (people and pets, search, hiding), person pages with their timeline, renaming, birthdays, portraits, merging, moving photos to someone else and reviewing suggestions, and a face editor in the viewer. -
search: every
/searchroute. Metadata, smart (clip) and random search, statistics, large assets, the smart search facets, explore, places, cities and the suggestion lists, all running gallery's flat search. Like the fork, the structuredfiltertree is validated and then refused. Smart search asks the machine learning server to encode the text, a request, not a job. Searches that would reach a shared space fail with a 500. The web app has a search palette in the header (/or ⌘K) with smart, file name, description and text modes and live people, place, album and tag matches, a search page with removable chips and a filter panel, "show similar photos" in the viewer, and explore and places pages. -
memories: every
/memoriesroute. The list with gallery's filters (date, saved, upcoming, trashed, type, paging, random order), statistics, one memory, create, update, delete and adding or removing photos. The list honours the admin's memory type switches and each user's, and leaves out archived, locked and hidden person photos. The nightlyMemoryGeneratejob is ported: on this day memories and every rule of the fork (recent trips, month and season recaps, favorites and person throwbacks, people together, places, video moments, themes found through clip search) with its overlap reservation, and birthday memories following Immich 3.3 (photos taken on the person's birthday over the years, shown from three days before), stored as the fork'sbirthdayrule memories so its mobile app shows them.MemoryCleanupkeeps the configured retention. The web app shows memories on explore, a memories page with its upcoming and favorites filters, and a memory viewer that plays each photo for five seconds, with favorite, remove, keyboard and swipe navigation and the memory's photos below. -
recently added: the timeline ordered and grouped by date added, like gallery's recently added page, reached from explore.
-
accounts and administration:
/users/meupdates,/users/me/preferences,/assets/statistics, every/admin/usersroute (list, create, get, update, delete, restore, sessions, statistics, preferences) and/system-config(read, defaults, save). The config is validated against gallery's own admin schema, dumped from its compiled zod intocrates/imora-core/src/system_config/gallery.json, and saved as the part that differs from the defaults like gallery does. A save tells gallery's nodes through the socket cluster, so they reload it at once. Creating a user withnotifyqueues the welcome email. The erasing job of a forced delete is left out, deleted accounts are finished after the configured delay. The web app has a sidebar (photos, explore with people, places, memories and recently added, favorites, albums with the recent ones, archive, settings and, for admins, users and system settings) that follows thesidebarWebpreferences, user settings (account, password and PIN, devices, API keys, preferences) and admin settings for authentication, server, memories, trash and account deletion. -
locked folder:
/lockedshows the locked timeline once the session is unlocked, otherwise/auth/pin-promptasks for the PIN (or creates the first one) and comes back. Photos move in and out from selections and the viewer with gallery's confirmations, locked photos are deleted for good rather than trashed, files dropped on the page upload as locked, and "Lock" locks the session again. -
notifications:
/notificationsand/admin/notifications(create, test email, template previews), theNotifyAlbumInvite,NotifyAlbumUpdate,NotifyUserSignup,SendMailandNotificationsCleanupjobs, and gallery's emails:crates/imora-emails/src/emails.jsonholds gallery's react email templates rendered with marker values, whichcrates/imora-emails/src/lib.rsfills the way react would, so the html is gallery's byte for byte, andcrates/imora-core/src/smtp.rssends them like nodemailer. The web app has the notification bell, the email switches in user settings, the email and template sections in system settings, and the welcome email switch. -
trash:
/trash/empty,/trash/restoreand/trash/restore/assets, and a/trashpage (when the trash is on) with the retention notice, restore all, empty trash, and restoring or deleting a selection. Deleting elsewhere trashes with an undo, or deletes for good after a confirmation when the photo is already trashed or locked, the trash is off, or with shift+delete. -
stacks: every
/stacksroute (search, create with gallery's merging of stacks whose primary joins, get, set the primary, delete one or many, remove an asset) and theon_asset_stack_updateevent. On Photos and Recently added a selection can be stacked (s) or un-stacked. The viewer adds upload to stack, un-stack, keep this and delete others, set as primary and remove from stack, arrow up and down through the stack, and previews a strip item on hover. -
mobile sync:
/sync/streamand/sync/ackwith every stream of gallery'sSYNC_TYPES_ORDER(users, partners and their photos with backfills, assets, exif, stacks, albums with their users and photos, people, faces, metadata, edits, ocr, memories, and the fork's shared space and library streams), acks, resets and the 30 day full sync. -
onboarding:
/users/me/onboarding,/system-metadataand/system-config/storage-template-options. The web app has the welcome page of a new server, admin registration, the forced password change, sign in withcontinuelinks, and onboarding steps for the theme (light, dark or the system's, also in preferences and on shift+t), and for admins the server privacy, storage template and backup steps. A storage template section joins system settings. -
maintenance:
/admin/maintenanceswitches the server into maintenance mode, where it restarts into gallery's maintenance server (server config, ping, version, status, install check, token login and ending it, through the login url it logs) and back, like gallery's workers restart. The integrity reports (/admin/integrity) list, download, export and resolve what the integrity checks found. The web app has the maintenance screen, the redirect into and out of it, and the admin maintenance and report pages. Restoring a database backup fails like a restore with a bad file, database backups are not planned. -
GET /users/{id}, and the fork'sGET /admin/users/{id}/library-manifest, a paged list of a user's originals with their checksums and albums for exports. -
map:
/map/markers,/map/reverse-geocode(countries named after GeoNames like Immich 3.3, the names already stored renamed at startup) and the fork's filtered/gallery/map/markers(gallery's flat search filters, person tokens included). The web app has a map page with clustered markers, the filter panel, a side timeline for a cluster or the visible area and the position in the url, a mini map in the details panel, and a location picker to set or change where photos were taken. At startup the microservices worker imports gallery's reverse geocoding data (GeoNames cities500 with its admin codes and the Natural Earth countries) fromIMORA_GEODATA(/build/geodataby default) like gallery'sMapRepository.init, when itsgeodata-date.txtdiffers from the last import. The import runs in one transaction, and a missing file or a failure keeps the places already imported. -
duplicates and utilities: every
/duplicatesroute (groups with gallery's keep suggestion, resolve merging favorites, visibility, rating, location, description, albums and tags into the kept photos, dismissing), and a utilities page with the duplicates review, large files and "manage location". -
assets:
/asset-files(search, get, download, delete),/assets/copy,/assets/jobs, asset metadata (/assets/metadataand/assets/:id/metadata) and the non destructive editor (/assets/:id/editswith gallery's validation, and itsAssetEditThumbnailGenerationjob for crops, turns, mirrors and video trims, byte for byte like gallery's, thenAssetEditReadyV2). Face and text boxes follow the edits likeutils/transform.ts. The web app has an editor in the viewer: crop with aspect ratios, rotate, flip and trim for videos. -
real time video: the fork's hls routes (
/assets/:id/video/stream/...) answer like gallery's api, and imora runs the transcoding worker side too, ffmpeg per session through gallery's server events on the socket cluster. The web player streams with hls.js when the feature is on. -
face cleanup: every
/admin/face-repairroute (the scan job, flagged faces, resolving by moving, keeping, locking, parking or detaching faces, dismissals, resolutions and their undo, owner people) and admin pages for the scan results, guided and manual review, and resolutions./server/ml-healthand gallery's machine learning availability checks come with it. -
machine learning: the
imora-mlbinary runs gallery's machine learning api (/ping,/predict) in rust, in place of the fork's python service: clip image and text embeddings (with the nllb language table), face detection and recognition (scrfd and arcface), ocr (pp-ocrv5 detection and recognition), and the fork's pet detection and recognition. It loads the same models from hugging face, runs them through onnx runtime with the same execution providers (openvino, cuda, rocm, cpu) and options, and unloads idle models the same way. The image preprocessing ports what the python code leans on, pillow's resampling, the opencv and numpy routines, openblas's small solves and numpy's float32expand sorts, so on 453 previews from the library every answer was byte for byte the one the python service gives onmain. The clip, face and ocr models follow Immich 3.3's service, byte for byte on its own onnx runtime: one graph per input shape where a provider compiles fixed shapes (openvino, cuda, tensorrt rtx), faces recognized four at a time and text lines six at a time, ocr widths and canvases from Immich's buckets, denormals flushed, Immich's optimizer exclusions, four threads for text search, models that load side by side, preloaded models kept, memory handed back after five idle seconds, and the TensorRT-RTX plugin first when its library sits beside onnx runtime. It always runs Immich'sv2model exports, Gallery's pet models aside, which only exist asmain:crates/imora-onnxports the runtime half of Immich'simmich-modellibrary, the fp16 derivation every provider but the cpu gets and each provider's graph rewrites, on the onnx protobuf itself, once per model in a child process. Its graphs are bit for bit those ofimmich-modelon 16 model towers and 6 providers, seedocs/benchmarks.md. The admin system settings have gallery's machine learning, classification, video, image, job, logging, map and the other sections. -
realtime: a websocket only socket.io 4 server at
/api/socket.iolike gallery's gateway (engine.io errors, cors, the client bundles, heartbeats,error: unauthorizedfor bad credentials), sending gallery's events from the same actions:on_server_versionandon_new_releaseon connect,on_session_delete,on_asset_trash,on_asset_restore,on_album_update,on_notificationandon_config_update. Imora joins gallery's@socket.io/redis-adaptercluster in its redis (crates/imora-core/src/socket/cluster.rs), so events from gallery's workers (finished uploads, portraits, metadata) reach imora's clients, imora's reach gallery's, and cluster requests are answered like a node would,AppRestartincluded. WithoutREDIS_*variables events stay on the one server. The web app applies timeline changes live (2.5 second batches like gallery), refreshes the open photo, new portraits and the admin user list, signs out when its session is deleted elsewhere, shows the server status and version in the sidebar, announces new releases to admins and waits out restarts. -
jobs: a bullmq compatible queue client and worker (
crates/imora-bullmq,crates/imora-core/src/jobs/, handlers incrates/imora-jobs) with gallery's queues, job names, options and follow ups, so imora and gallery can work each other's jobs. The ported jobs are thumbnails (libvips loaded at runtime, following sharp's pipeline, and ffmpeg for videos), metadata extraction (exiftool with exiftool-vendored's parsing and time zone rules), sidecar check and write, smart search, duplicate detection, face detection, facial recognition, the people identity backfill and suggestion scans, OCR, pet detection and recognition, auto classification, video transcoding (software, NVENC, QSV, VAAPI and RKMPP), the storage template migration, file migration, and the trash (AssetEmptyTrash,AssetDeleteCheck,AssetDeletewith its stack, live photo and file cleanup), and memory generation and cleanup.crates/imora-jobs/src/nightly.rsqueues gallery's nightly jobs at the configured start time when no gallery api holds its advisory lock, imora skips the ones it has no handler for. The user purge (UserDeleteCheck,UserDelete) removes deleted users after the configured delay, or right away when deleted with force, and tells clients withon_user_delete. The integrity checks (crates/imora-admin/src/integrity.rs) report untracked, missing and changed files on their configured cron schedules, and the admin maintenance page shows and resolves the reports. External libraries and backups are not ported. Like gallery, imora needs its redis (REDIS_*,redis:6379by default) and always queues the jobs gallery's api queues.IMORA_WORKERS_INCLUDEandIMORA_WORKERS_EXCLUDE, Gallery'sIMMICH_WORKERS_*, pick which of its two workers the process runs:apiserves http and websockets,microservicesworks the queues, both by default. The queues are gallery's, under imora's prefix (imora_bull): an immich or gallery install'simmich_bullkeys are renamed on the first start, so its pending work carries over. Production runs both workers.
API compatibility
docs/comparison.md lists every Gallery API operation and every user facing Gallery web feature, each marked as
done, partial or missing in Imora. Features Imora will not implement (external libraries, database backups, shared spaces,
workflows, Takeout and iCloud imports, licensing, folders, tags, partners, user groups and the Android APK links) are
kept apart under "Not planned" at its end.
Clients detect Imora from GET /api/server/version, where Imora adds product: "imora" and features, the optional
api areas it implements, after Gallery's fields. A client gates on that list rather than on a version.
Imora answers like Noodle Gallery 5.7.0 on the routes it implements, except that it reports its own
version (0.0.0), checks for releases at https://code.vexcited.com/imora/imora, and uses its own names where
Gallery uses Immich's: the x-imora-* headers, the imora_* cookies, /.well-known/imora and the
app.imora:///oauth-callback mobile redirect. Status codes, bodies, validation errors (zod issues), json syntax
errors (v8 messages), ETags, the helmet and correlation id headers and the express 404 page are Gallery's.
Tokens, API keys, PIN codes and passwords are stored the same way, so a database taken over from Gallery keeps
its sessions and keys. The official Immich and Gallery apps do not connect to Imora, its own apps do.
This was checked against Gallery 5.7.0 by replaying full auth flows, single requests, every job and the socket
traffic on both servers sharing one database, and diffing the answers, the rows and the files left behind, see
docs/comparison.md. What stays in the repository are the recorded answers the tests replay:
crates/imora-media/src/ffmpeg-cases.json.gz holds 3000 ffmpeg commands and errors from gallery's own builder, and
crates/imora-core/tests/v8_json_errors.json the JSON.parse messages of node.
Development
Requirements: Rust nightly, NASM (libjpeg-turbo is built without its simd code otherwise), Bun, Node 22.12+ (Vite), PostgreSQL with pgvector or VectorChord, and Redis for the job queues and the socket cluster.
cp .env.example .env # then point DB_URL and REDIS_* at your database and redis
cargo server # api on http://localhost:2284
cargo ml # optional, machine learning on http://localhost:3003
cd web && bun run dev # web app on http://localhost:5173, proxies /api
cargo test # unit tests
cd web && bun run build # type check and build
The server serves the web app it was built with: web/dist is compiled into the binary, so build the web app
before a release build. IMORA_WEB_ROOT serves another folder instead. The server listens on 2283 unless
IMORA_PORT says otherwise, .env.example moves it to 2284 for development.
Imora never migrates the database: the schema belongs to Immich. On a database with no tables it creates the one
Gallery 5.7.0 creates on its first start, migration rows included, so Gallery accepts it later: Gallery's own
database after its migrations, dumped once with pgvector and once with VectorChord into
crates/imora-core/src/db/bootstrap/. Imora picks the dump like Gallery picks its extension
(DB_VECTOR_EXTENSION, else VectorChord before pgvector), under Gallery's migration lock. A database with tables
is left as it is. Queries go through tokio-postgres with a deadpool-postgres pool, each statement prepared once
per connection. The auth and settings rows are plain structs with their own sql
(crates/imora-core/src/db/models.rs), everything else is sql written after Gallery's queries.
Deployment
Production is the Proxmox container pct 106 (named immich), installed like proxmox/ does it, so its
update command installs the latest release like any community-scripts container. Releases deploy by themselves:
Forgejo's action_run_success webhook of a release run reaches the container's webhook server, whose hook in
/srv/hooks runs update. deploy/deploy.sh puts a build of this checkout there in between
releases: it cross compiles both binaries with cargo zigbuild and swaps /opt/imora/bin, until the next
update. The container is laid out like the Proxmox installer does it: the imora user, /opt/imora/.env, the media in
/opt/imora/upload, the model cache in /opt/imora/cache, onnx runtime with OpenVINO in
/opt/imora/lib/onnxruntime and exiftool 13.59 in /opt/imora/lib/exiftool. libvips and its codecs are in
/usr/local/lib and jellyfin's ffmpeg in /usr/lib/jellyfin-ffmpeg. Caddy (pct 101) serves it at
https://photos.vexcited.com.
Machine learning on another machine
The server only talks to imora-ml over http, through the urls in System settings > Machine
learning, so the service can run on any machine with a faster cpu or a gpu. Build it with
cargo build --release -p imora-ml, then run it with the same variables gallery's python service reads:
| Variable | Default | Meaning |
|---|---|---|
IMORA_ML_HOST, IMORA_ML_PORT |
[::], 3003 |
address to listen on, IMMICH_HOST and IMMICH_PORT in Gallery's service |
MACHINE_LEARNING_CACHE_FOLDER |
~/.cache/imora_ml |
where models are downloaded |
IMORA_ONNXRUNTIME_PATH |
libonnxruntime.so (.dylib on macos) |
the onnx runtime library, a build with the execution providers wanted (openvino, cuda, rocm) |
IMORA_TENSORRT_RTX_PATH |
beside onnx runtime | the onnxruntime-ep-nv-tensorrt-rtx plugin, used before cuda on ampere and newer gpus |
MACHINE_LEARNING_OPENVINO_PRECISION |
the device's own | the precision openvino runs at, imora's own; the pet models run at FP32 |
MACHINE_LEARNING_* |
gallery's | the rest of gallery's settings: idle unloading, preloading, threads and device |
Then add its url (for example http://gpu-box:3003) in System settings > Machine learning. Imora pings
every url, sends requests to the first healthy one and falls back to the next, so the local service can
stay listed after it.
Releases
npx --@mentor:registry=https://code.vexcited.com/api/packages/mentor/npm/ @mentor/release
(@mentor/release lives on code.vexcited.com's npm registry, not npmjs.org) bumps the version of the workspace in
Cargo.toml and Cargo.lock, commits chore: release vX.Y.Z, tags it X.Y.Z and pushes both. Pushing
the tag runs .forgejo/workflows/release.yml on the ubuntu-amd64 and ubuntu-arm64 runners: it creates the
release, attaches imora-X.Y.Z-linux-<amd64|arm64>.tar.gz with its .sha256, and pushes the images to
code.vexcited.com/imora. The images are built from docker/ around the binaries of the same run. Pushing
packages needs the REGISTRY_USERNAME and REGISTRY_TOKEN secrets, a Forgejo user and one of its tokens with
the write:package scope.
A successful release run then deploys production: Forgejo sends its action_run_success webhook, signed with a
shared secret, to the webhook server of the production container, and its hook runs update there when the run's
tag is the latest release.