Self-hosted photo and video gallery with AI-powered search, grouping, and tagging
Upload photos and videos, organize them into folders and use integrated AI tools for search and sorting — all running on your own hardware. No cloud, no API keys, fully private. High performance, low resource usage, and only a single container without any other dependencies.
|
|
|
|
-
Visual Search — Find similar photos and videos by uploading a reference image, with adjustable similarity threshold and one-click grouping
-
Auto Tagging — Tag a few items in the library and let the AI automatically label matching items across your library
-
Duplicate Detection — Duplicates are detected during upload and silently skipped
-
Virtual Folders — Organize media into folders without moving files; one item can live in multiple folders with drag-and-drop support
-
Favorites — Mark items as favorites for quick access in a dedicated view
-
Multi-Select & Batch Operations — Marquee selection, shift-click, batch download (auto-split zip), batch delete, and batch add-to-folder
-
Video Support — Upload any common video format with automatic frame extraction for thumbnails and AI features
-
EXIF Metadata — View camera details, date, GPS, exposure, and more
-
Deep Linking — Bookmarkable URLs for folders, favorites, search states, and individual items
-
Password Protection — Optional single-password auth with rate limiting and secure sessions. No complex user management, only a simple password.
-
Responsive UI — Infinite-scroll grid, keyboard shortcuts, touch swipe, and full mobile support. Includes a persistent thumbnail resizer (S/M/L) to customize your viewing experience.
-
Drag-and-Drop Upload — Drag files anywhere into the browser window to upload. Context-aware: dropping into a virtual folder automatically adds the files to that folder.
-
Real-time Sync — WebSocket-powered instant updates across all browser clients; all users can see new uploads, favorite toggles, and folder changes immediately as they happen
-
Self-Healing — Automatically detects and repairs missing thumbnails or metadata in the background
-
100% Self-Hosted — No cloud, no telemetry. Your data stays yours.
| Feature | Technology |
|---|---|
| Visual Search | MobileNetV3-Large extracts 1280-dim feature vectors; cosine similarity via sqlite-vec |
| Similarity Grouping | Agglomerative clustering over the same embedding space with a user-adjustable distance threshold |
| Auto-Tagging | Linear SVM with Platt-calibrated probabilities trained on user-provided examples via linfa-svm |
| Duplicate Detection | Perceptual hashing (image_hasher) compared at upload time |
| Video Processing | ffmpeg thumbnail filter selects visually distinct frames for thumbnails, hashing, and embeddings |
| AI Inference | ort (ONNX Runtime) for fast CPU-based model execution |
| Batch Downloads | Real-time ZIP streaming via async_zip with automatic partitioning into ~2 GB parts |
| Authentication | Argon2-hashed password, rate-limited login, secure HTTP-only cookies |
The easiest way to run GalleryNet is with Docker:
docker run -d \
--name gallerynet \
-p 3000:3000 \
-v gallerynet-data:/app/data \
-e DATABASE_PATH=/app/data/gallery.db \
-e UPLOAD_DIR=/app/data/uploads \
-e THUMBNAIL_DIR=/app/data/thumbnails \
-e GALLERY_PASSWORD=your-secret-password \
sedrad/gallerynetservices:
gallerynet:
image: sedrad/gallerynet
container_name: gallerynet
ports:
- "3000:3000"
environment:
- DATABASE_PATH=/app/data/gallery.db
- UPLOAD_DIR=/app/data/uploads
- THUMBNAIL_DIR=/app/data/thumbnails
- GALLERY_PASSWORD=your-secret-password
volumes:
- ./data:/app/data
restart: unless-stoppeddocker compose up -d| Variable | Default | Description |
|---|---|---|
DATABASE_PATH |
gallery.db |
Path to the SQLite database file |
UPLOAD_DIR |
uploads |
Directory for original uploaded files |
THUMBNAIL_DIR |
thumbnails |
Directory for generated thumbnails |
MODEL_PATH |
assets/models/mobilenetv3.onnx |
Path to the ONNX model file |
GALLERY_PASSWORD |
(empty) | Set to enable password authentication. Leave empty for no auth |
CORS_ORIGIN |
(empty) | Set to allow cross-origin requests from a specific origin (e.g. https://example.com). Unset = same-origin only |
- Rust — Latest stable toolchain
- Node.js — v18+
- ffmpeg — Must be on PATH for video support
# Clone the repository
git clone https://github.com/srad/GalleryNet.git
cd GalleryNet
# Build the frontend
cd frontend && npm install && npm run build && cd ..
# Run the server
cargo run --releaseThe server starts on http://localhost:3000.
# Backend (auto-reload with cargo-watch)
cargo watch -x run
# Frontend (Vite dev server with HMR, proxies /api to :3000)
cd frontend && npm run devGalleryNet follows Hexagonal Architecture with clean separation of concerns:
src/
├── domain/ # Models & trait ports (zero dependencies)
├── application/ # Use cases (upload, search, list, delete) and background tasks
├── infrastructure/ # SQLite, ONNX Runtime, perceptual hashing
├── presentation/ # Axum HTTP handlers & auth middleware
└── main.rs # Wiring & server startup
| Layer | Technology |
|---|---|
| Backend | Rust, Axum, Tokio, WebSockets |
| Database | SQLite + sqlite-vec | | AI/ML | ort (ONNX Runtime), MobileNetV3-Large, linfa-svm (tag learning) | | Frontend | React 19, TypeScript, Tailwind CSS v4, Vite | | Video | ffmpeg (frame extraction) | | Hashing | image_hasher (perceptual hashing) |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/upload |
Upload media (multipart). Returns MediaItem. 409 for duplicates |
POST |
/api/search |
Visual similarity search. Multipart with file + similarity |
GET |
/api/media |
Paginated media list. Params: page, limit, media_type, sort |
GET |
/api/media/{id} |
Get single media item with EXIF data |
POST |
/api/media/{id}/favorite |
Toggle favorite status. Body: {"favorite": true/false} |
DELETE |
/api/media/{id} |
Delete single media item |
POST |
/api/media/batch-delete |
Batch delete. Body: ["uuid1", ...] |
POST |
/api/media/fix-thumbnails |
Trigger background repair of missing thumbnails/metadata |
POST |
/api/media/download/plan |
Create download plan (partitions large sets into <2GB parts). Body: ["uuid1", ...] |
GET |
/api/media/download/stream/{id} |
Stream a specific download part incrementally |
POST |
/api/media/download |
Simple batch download (if under 2GB). Body: ["uuid1", ...] |
GET |
/api/tags |
List all unique tags |
GET |
/api/tags/count |
Count auto-tags in current view |
POST |
/api/tags/learn |
Train model from manual tags. Body: {"tag_name": "..."} |
POST |
/api/tags/auto-tag |
Apply all trained models to current scope |
GET |
/api/folders |
List all folders with item counts |
POST |
/api/folders |
Create folder. Body: {"name": "..."} |
DELETE |
/api/folders/{id} |
Delete folder (keeps media files) |
GET |
/api/folders/{id}/media |
Paginated media in folder |
POST |
/api/folders/{id}/media |
Add media to folder. Body: ["uuid1", ...] |
POST |
/api/folders/{id}/media/remove |
Remove media from folder |
GET |
/api/folders/{id}/download |
Get download plan for folder (auto-splits for large folders) |
GET |
/api/stats |
Server statistics (counts, storage, disk space) |
POST |
/api/login |
Authenticate. Body: {"password": "..."} |
POST |
/api/logout |
Clear session |
GET |
/api/ws |
WebSocket for real-time library synchronization |
GET |
/api/auth-check |
Check authentication status |
Contributions are welcome! Feel free to open an issue or submit a pull request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/my-feature) - Commit your changes (
git commit -m 'Add my feature') - Push to the branch (
git push origin feature/my-feature) - Open a Pull Request
PolyForm Noncommercial License 1.0.0 means it is free for non-commecial purposes.




