Documentation
How to get Imadeo running, and what each part of it does once it is.
Install with Docker
Imadeo runs as six containers: web, API, background processing, Postgres, Redis, and machine learning. They come up together from one compose file.
Download only the Docker installation files and keep them together:
mkdir imadeo && cd imadeo
curl -fsSLO https://raw.githubusercontent.com/eranabir/imadeo/master/docker/docker-compose.yml
curl -fsSLO https://raw.githubusercontent.com/eranabir/imadeo/master/docker/generate-secrets.sh
curl -fsSL https://raw.githubusercontent.com/eranabir/imadeo/master/docker/.env.example -o .env.example
chmod +x generate-secrets.sh
./generate-secrets.sh
docker compose up -d
The generator creates .env with cryptographically random
database, Redis, JWT and Locked-folder keys. It keeps any value that was
already configured, so it is safe to run again. Media, database files,
and model files stay in this installation folder by default.
Imadeo exposes one LAN/VPN port, 1111, for both the web app
and mobile API. It does not ship a public reverse proxy.
Remote access with a reverse proxy
On a LAN or VPN, open http://<server-ip>:1111 and connect
the mobile app to that same address. For internet
access, put a reverse proxy in front of web port 1111.
The proxy terminates HTTPS and must preserve Host,
X-Real-IP, X-Forwarded-For, and
X-Forwarded-Proto; allow uploads as large as Imadeo permits.
photos.example.com {
reverse_proxy http://192.168.1.20:1111
}
With Nginx Proxy Manager or a custom Nginx proxy, add these advanced directives so large files are not reported as a 504 while the NAS is still verifying them:
client_max_body_size 50G;
proxy_request_buffering off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
Set the public HTTPS address in .env:
PUBLIC_URL=https://photos.example.com
LOCAL_HTTP_ENABLED=true
Forward only the proxy’s HTTPS port through the router. Never expose
1111 directly. A raw WAN IP cannot use
a normally trusted certificate, so remote access needs a domain/DDNS name
or a VPN.
What each service does
| Service | Purpose |
|---|---|
web | The browser interface and same-origin proxy to the API. |
server | The API. It stores originals and remains responsive while background work runs elsewhere. |
worker | Reads metadata, creates thumbnails, optimises videos and schedules local analysis with bounded resources. |
database | Postgres with pgvector, which stores both the metadata and the image embeddings that make search work. |
redis | The job queue. Thumbnails, metadata extraction, face detection and video transcoding all run through it. |
machine-learning | Face detection and the image/text model behind search. Runs locally; nothing is sent anywhere. |
TrueNAS SCALE
SCALE runs ordinary Docker, so the compose file above works as-is. Since 24.10 the apps system is Docker Compose rather than Kubernetes, which means Imadeo installs the same way it does anywhere else.
Create two datasets first
Imadeo needs two, not one. The library is the obvious half; the database is the half people miss, and losing it means the files survive but every album, name and favourite is gone. A parent with two children keeps them together:
| Dataset | Holds |
|---|---|
/mnt/tank/imadeo/data | One isolated media tree for every registered user. |
/mnt/tank/imadeo/database | Postgres. Albums, names on faces, favourites — everything you have organised. |
Both go in .env. Neither default is safe on SCALE:
DB_DATA_LOCATION is a path relative to the compose file, so
left alone it puts your database wherever you happened to unpack things.
UPLOAD_LOCATION=/mnt/tank/imadeo/data
DB_DATA_LOCATION=/mnt/tank/imadeo-postgres
Install
From Apps → Discover → Custom App, choose the option to install via YAML and paste the compose file. Or from a shell, exactly as on any other machine:
mkdir -p /mnt/tank/apps/imadeo && cd /mnt/tank/apps/imadeo
curl -fsSLO https://raw.githubusercontent.com/eranabir/imadeo/master/docker/docker-compose.yml
curl -fsSLO https://raw.githubusercontent.com/eranabir/imadeo/master/docker/generate-secrets.sh
curl -fsSL https://raw.githubusercontent.com/eranabir/imadeo/master/docker/.env.example -o .env.example
chmod +x generate-secrets.sh
./generate-secrets.sh
docker compose up -d
.env on a data pool, not on the
boot pool. A SCALE upgrade can reset the boot pool, and anything stored
there goes with it.
Where the library lives
UPLOAD_LOCATION in .env is the setting that
matters most, and the one to get right before the first upload. It is the
host directory bound into the container, and everything Imadeo writes
lives under it.
UPLOAD_LOCATION=/volume1/imadeo-data
| Directory | Holds |
|---|---|
users/<user-id>/library/ | That user's original files, untouched. |
users/<user-id>/thumbs/ | That user's generated thumbnails and previews. |
users/<user-id>/encoded-video/ | That user's transcoded playback copies. |
users/<user-id>/profile/ | That user's profile picture. |
backups/ | Database dumps. |
users/<user-id>/upload/ | That user's uploads in progress. |
Moving an existing library
Stop the stack, move the directory, point UPLOAD_LOCATION at
its new path, and start again. Paths are stored relative to the library
root, so nothing needs rewriting in the database.
docker compose down
mv /volume1/imadeo-data /volume2/imadeo-data
# set UPLOAD_LOCATION=/volume2/imadeo-data in .env
docker compose up -d
First run
Open the server's address in a browser. A server with no accounts shows the registration screen instead of a login, because the only thing that makes sense on an empty server is creating the first administrator.
That first account is the admin. Every account created afterwards — by invitation or directly — is an ordinary user with its own separate library.
Uploading
Upload in the header takes individual files, or a whole folder. Choosing a folder brings its structure across intact rather than flattening everything into one pile.
Uploads are checked against what you already have. If a file matches something in your library, Imadeo says so rather than silently making a second copy — but you can override that and keep the duplicate, which keeps its original filename and gets its own entry.
The untouched original is committed before an upload reports success. The browser supplies a small temporary preview, so new photos can appear without waiting for the server. After the upload batch and recent foreground activity finish, durable jobs read metadata and create canonical thumbnails. Video optimisation, search and People & Pets then use a one-at-a-time heavy lane; they never compete with thumbnail creation. Restarting the worker does not lose queued work.
API and processing have separate CPU and memory limits. The defaults
suit a small NAS; larger servers can raise WORKER_CPU_LIMIT,
WORKER_MEMORY_LIMIT and
JOB_BACKGROUND_CONCURRENCY. Settings → Processing shows
whether the worker is active, waiting for uploads, or offline.
The timeline
Photos is everything you have, grouped by the day it was taken. The rail down the right side lists the years in your library; clicking one jumps to it.
Selecting
- Click the circle on a photo to select it.
- Shift-click selects everything between two photos, across day boundaries.
- Hover a date heading and a circle appears beside it — that takes the whole day at once.
- With a selection active, the bar at the bottom can favourite, download, or move to trash.
Sorting
The default is date taken. Recently added is the useful one after an upload: a photo shot in 2009 files itself under 2009 and can look like it never arrived, and sorting by when it was uploaded answers that.
Albums and folders
These are different things, deliberately.
- Folders mirror the directory structure your files arrived in. Upload a tree and it comes across as it was.
- Albums are sets you assemble yourself. A photo can be in several albums and still sit in its original folder.
Right-clicking a photo, or a selection, offers to add it to an album.
People & Pets
Faces are detected as photos arrive and grouped by who they belong to. Cats and dogs are recognised separately, so they appear in the Pets tab rather than among people. Nobody is named at first — open People & Pets, click a person or pet, and give it a name. Every photo of that subject is grouped from then on.
- Merge combines two groups that turned out to be the same person or pet.
- Not this person or pet corrects a wrong match. The photo is kept; only the association is removed, and it will not come back on the next pass.
- Use as cover picks which photo their thumbnail is cropped from.
- Hiding a person or pet removes them from the grid without touching any photos.
You can also assign a photo to someone by hand, from the right-click menu, whether or not a face was detected in it.
New uploads are analysed automatically. Administrators can use Settings → Recognition → Scan again to run the current library through recognition again after changing settings or correcting older results. If a photo is moved to Trash, it is removed from the group immediately and a surviving photo becomes the group cover.
Search
The header search field looks at filenames, descriptions and dates. The icon at its right opens the advanced options.
| Search type | Matches |
|---|---|
| Context | What a photo shows. Describe the picture — "a dog on a beach" — and it is matched against the image itself, with no tags or filenames involved. |
| File name or extension | The original filename, or a file type such as .mov. |
| Description | Text you have written on a photo. |
| Album or folder | The name of an album or folder. |
Any of these can be narrowed to a date range, or to particular people and pets.
Duplicates
Duplicates groups photos that are the same picture rather than the same file. A resized copy, a re-saved export and a version that came back through a messaging app all match, even though their bytes differ and no two filenames agree.
Nothing is deleted for you. Each group shows what it found so you can keep the one you want.
Locked
Photos moved to Locked are kept out of the timeline, search results and albums. They are reachable only from that section.
Users and invitations
Under Settings → Users, an administrator can invite someone by email or create a user directly.
- Invitation sends a link that opens registration with the address already filled in. It requires email to be configured.
- Create a user sets up the account and password yourself, with no email involved.
Each account has its own library. People do not see each other's photos unless something is shared with them.
Email is optional. Without it Imadeo works normally, but invitations and address-change confirmations cannot be sent — the invite button stays disabled until a server is configured.
Settings takes ordinary SMTP details from your provider. One field is easy to miss: the public address of your server. Links in outgoing email are built from it, so if it is left pointing at localhost, an invitation will work for you and for nobody else.
# reachable from wherever the recipient opens the link
https://photos.example.com
Backing up
Two things need backing up, and they are not the same:
- The library directory — your actual photos and videos. This is the part that cannot be regenerated.
- The database — albums, names on faces, favourites, and everything else you have organised. Losing it means the files survive but the organisation does not.
Thumbnails and transcoded video are derived and can be rebuilt, so they are not worth the backup space.
docker compose exec -T database pg_dump -U "$DB_USERNAME" "$DB_DATABASE_NAME" \
> imadeo-$(date +%F).sql