The bulk action panel previously covered rating, color, flag, and pick but had no way to apply tags across a multi-photo selection — the only path was to tag photos one at a time via the single-photo PhotoInfoPanel. Add it. - backend: extend the existing /photos/bulk action endpoint with add_tags and remove_tags actions. add_tags is idempotent (computes the new (photo_id, tag_id) pair set against existing rows and inserts only the missing ones); remove_tags is a single DELETE WHERE IN. - api.ts: bulkAddTags / bulkRemoveTags wrappers. - RightSidebar: new BulkTagsEditor below the bulk flag row. Filters / searches the existing tag list, lets the user click any chip to apply it to the whole selection or X to remove it. Typing a name with no exact match shows a "Create and apply" button that creates the tag via tagsApi.create and immediately attaches it to every selected photo. All three mutations invalidate both the photo and tag caches so the FilterBar tag count + sidebar Tags section stay fresh. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mulita - Self-Hosted Photo Management Application
A self-hosted, Docker-deployed photo management application inspired by Lightroom's workflow. Mulita provides a fast, keyboard-driven interface to browse, organize, tag, and manage your photo library.
Features
- Photo Organization: Browse photos in a timeline view with virtual scrolling for performance
- Thumbnail Generation: Automatic thumbnail generation for all photo formats including RAW
- Metadata Extraction: Full EXIF/XMP metadata extraction and search
- Keyboard Shortcuts: Lightroom-style keyboard navigation and actions
- File Support: JPEG, PNG, RAW formats (CR2, CR3, NEF, ARW, etc.), HEIC/HEIF, and videos
- Heaps: Temporary collections for organizing photos
- Tags & Ratings: Organize with tags, star ratings, and color labels
- Dark Mode: Photography-optimized dark interface
Tech Stack
Backend
- Python 3.12 with FastAPI
- SQLite with SQLAlchemy (async)
- Celery + Redis for background tasks
- pyvips for fast thumbnail generation
- ExifTool for metadata extraction
Frontend
- React 18 with TypeScript
- Vite for fast development
- TanStack Query for data fetching
- TanStack Virtual for virtualized scrolling
- Tailwind CSS for styling
- Zustand for state management
Quick Start
Prerequisites
- Docker and Docker Compose
Setup (one variable)
- Clone the repo:
git clone <repository-url>
cd muleimage
-
Set one environment variable in
.env— the host directory that contains your photo library. Whatever you point at will become your library inside Mulita.# macOS / Linux PHOTO_DIRS=/Users/you/Pictures # or any folder PHOTO_DIRS=/mnt/nas/photos # Windows (WSL) PHOTO_DIRS=/mnt/c/Users/you/Pictures -
Start the stack:
docker compose up -d
- Open
http://localhost:3000. On first boot Mulita will:- Mount your
PHOTO_DIRSat/photosinside the container - Auto-create a source root called Library pointing at
/photos - Queue an initial scan, generate thumbnails, and start serving them
- Mount your
You don't need to touch mulita.yml or the API to get started.
How libraries are managed
Mulita is config-driven: the host directory you mount via
PHOTO_DIRS becomes your library, and the backend automatically
registers it as a source root on startup. There is no UI for adding
or removing source roots — to change what Mulita scans, edit .env
(or docker-compose.yml for multi-mount setups) and restart the
stack.
This keeps the model simple: the docker mount IS the library. No two layers, no confusion about which view to use.
Changing or adding libraries
To point at a different library:
- Edit
PHOTO_DIRSin.env docker compose down- (Optional, for a clean slate)
docker volume rm muleimage_db_data muleimage_thumbs_data muleimage_proxies_data docker compose up -d
The new library shows up automatically. Without step 3 the old library's metadata stays in the DB and you'll see a warning at startup that the old source root's path is missing on disk — that's a hint to clean up.
For multiple libraries, edit docker-compose.yml and add additional
mount lines:
volumes:
- ${PHOTO_DIRS}:/photos:rw
- /Volumes/Archive:/archive:rw # additional library
Each mounted directory will need a corresponding source root row in
the DB; today that means POST /api/v1/folders via curl, or wait
for the multi-mount auto-registration that's on the roadmap.
Read-only libraries
The default mount is :rw because file operations (rename, move,
empty discard pile) need to mutate the filesystem. If you want a
strict read-only library — pointing at a network share, an
authoritative archive, etc. — flip :rw to :ro in
docker-compose.yml. Mulita will keep working for browsing, rating,
color labels, picks, heaps, and the (soft) discard flag, but the
following will return an OS error:
PATCH /photos/{id}with a newfilename(rename)POST /photos/move(bulk move)DELETE /discard/empty(file unlinks)
Heads up: with :rw, Mulita has full write access to whatever
host directory you mount. Treat the same way you would Lightroom's
catalog folder.
Architecture
The application consists of 5 Docker services:
- frontend: React SPA served by Nginx
- backend: FastAPI REST API
- worker: Celery workers for background tasks
- redis: Message broker for Celery
- db: SQLite database (file-based)
Keyboard Shortcuts
| Key | Action |
|---|---|
← → ↑ ↓ |
Navigate photos |
Space |
Quick preview |
Enter |
Open loupe view |
P |
Pick photo |
X |
Reject photo |
1-5 |
Set star rating |
Tab |
Toggle left sidebar |
I |
Toggle metadata panel |
G |
Grid view |
E |
Loupe view |
Delete |
Move to trash |
Development
Backend Development
cd backend
pip install -r requirements.txt
uvicorn app.main:app --reload
Frontend Development
cd frontend
npm install
npm run dev
Configuration
Source roots are managed by the UI / API (the database owns them). Edit
mulita.yml to configure operational settings only:
- Thumbnail sizes, quality, and format
- Scanner behaviour (watch, batch size, initial scan)
- Performance tuning (concurrency, cache TTLs, DB pool)
Performance
- Handles 100,000+ photos efficiently
- Virtual scrolling for smooth timeline navigation
- Thumbnail generation at 10+ photos/second
- SQLite FTS5 for fast full-text search
Future Features (Phase 2)
- AI-powered scene classification
- Face detection and clustering
- Smart albums
- Duplicate detection
- Export presets
- Multi-user support
License
MIT