A lightweight, self-hosted image gallery for AI-generated images.
- 🍲 Simple: Flask-based, minimal dependencies
- 📱 Mobile-friendly: PWA-ready responsive design
- 🔐 Authentication: OIDC (Authentik, Okta, Google) + local password
- 🖼️ Thumbnails: Auto-generated thumbnail caching
- 🗑️ Bulk operations: Multi-select delete
- 🔄 Refresh: Live refresh button
- 🐳 Containerized: Docker + Kubernetes deployment ready
docker run -d --name miso-gallery \
-p 5000:5000 \
-v /path/to/images:/data \
ghcr.io/misospace/miso-gallery:latestservices:
miso-gallery:
image: ghcr.io/misospace/miso-gallery:latest
ports:
- "5000:5000"
volumes:
- ./images:/data
environment:
- ADMIN_PASSWORD=your-password| Variable | Required | Default | Description |
|---|---|---|---|
DATA_FOLDER |
No | /data |
Path to image directory |
IMAGE_BASE_URL |
No | - | Base URL for shareable links |
PORT |
No | 5000 |
Server port |
Miso Gallery supports two authentication methods:
docker run -d \
-e ADMIN_PASSWORD=your-password \
ghcr.io/misospace/miso-gallery:latestdocker run -d \
-e AUTH_TYPE=oidc \
-e OIDC_ISSUER=https://authentik.yourdomain.com \
-e OIDC_CLIENT_ID=miso-gallery \
-e OIDC_CLIENT_SECRET=your-secret \
-e OIDC_CALLBACK_URL=https://miso-gallery.yourdomain.com/auth/callback \
-e SECRET_KEY=your-session-secret \
ghcr.io/misospace/miso-gallery:latest| Variable | Required | Default | Description |
|---|---|---|---|
AUTH_TYPE |
No | local |
Auth method: local, oidc, or none |
ADMIN_PASSWORD |
If local | - | Password for local auth (plaintext or Werkzeug hash: pbkdf2: / scrypt:) |
SECRET_KEY |
Yes | - | Flask secret for sessions. Generate with: python -c "import secrets; print(secrets.token_urlsafe(48))" |
LLM_READ_API_KEYS |
No | - | Comma-separated Bearer tokens with read scope (list, view, thumbnails). Write-scoped keys are also accepted here. |
LLM_WRITE_API_KEYS |
No | - | Comma-separated Bearer tokens with write scope (delete, dedup, bulk operations, task execution). |
LLM_API_KEYS |
No | — | Legacy single var; functions as both read and write. Deprecated in favour of LLM_READ_API_KEYS / LLM_WRITE_API_KEYS. |
OIDC_ISSUER |
If OIDC | - | OIDC provider URL (e.g., https://authentik.example.com) |
OIDC_CLIENT_ID |
If OIDC | - | OIDC client ID |
OIDC_CLIENT_SECRET |
If OIDC | - | OIDC client secret |
OIDC_CALLBACK_URL |
If OIDC | - | Callback URL for OIDC |
| Variable | Required | Default | Description |
|---|---|---|---|
RATE_LIMIT_REDIS_URL |
No | - | Redis/Dragonfly URL for shared rate-limit state (falls back to in-memory if unset/unreachable) |
RATE_LIMIT_PREFIX |
No | miso-gallery:ratelimit |
Key prefix for rate-limit entries |
RATE_LIMIT_ROUTE_LIMITS |
No | - | JSON overrides per endpoint, e.g. {"auth":{"max_requests":5,"window":300}} |
See docs/rate-limit-shared-backend.md for recommended production rollout and migration plan.
- Create an Application in Authentik
- Create a Provider (OpenID Connect) with these settings:
- Client ID:
miso-gallery - Client Secret: Generate a secure secret
- Signing Key: Select default
- Redirect URIs:
https://miso-gallery.yourdomain.com/auth/callback
- Client ID:
- Copy the Provider URL (issuer) to
OIDC_ISSUER
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: miso-gallery
namespace: apps
spec:
chart:
spec:
chart: app-template
version: 3.0
values:
controllers:
miso-gallery:
containers:
app:
image:
repository: ghcr.io/misospace/miso-gallery
tag: latest
env:
- name: ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: miso-gallery-secrets
key: password
persistence:
data:
type: nfs
server: nfs.yourdomain.com
path: /path/to/images
service:
app:
ports:
http:
port: 5000
route:
app:
hostnames:
- gallery.yourdomain.com
parentRefs:
- name: envoy-externalThumbnails are automatically generated and cached in .thumb_cache/ directory. This improves loading performance for large galleries.
- Max size: 400x400
- Format: Optimized JPEG
- Auto-refresh: Thumbnails regenerate when source image changes
- Click checkboxes on images to select
- Use "Select All" / "Deselect All" buttons
- Bulk delete selected images
Even when authentication is enabled, direct URLs to images remain publicly accessible:
/view/folder/image.jpg- Full resolution/thumb/folder/image.jpg- Thumbnail
This allows sharing images while protecting the gallery UI.
Miso Gallery includes a JSON API intended for LLM agents and other machine-to-machine clients. The primary purpose is to let an external LLM client inspect and manage gallery state: list/search media, read metadata, tag, delete, bulk-delete, and deduplicate images.
Enable the API by configuring one or more API keys. The desired model:
- Read keys (
LLM_READ_API_KEYS) grant access to list, view, and thumbnail endpoints. - Write keys (
LLM_WRITE_API_KEYS) grant access to delete, dedup, bulk operations, and task execution. A write key also works on read endpoints (write implies read). - Legacy keys (
LLM_API_KEYS) function as both read and write — supported for backward compatibility but deprecated.
Prefer separate read and write keys for new deployments; LLM_API_KEYS remains supported as a legacy all-purpose key. When explicit LLM_READ_API_KEYS or LLM_WRITE_API_KEYS are set, the legacy LLM_API_KEYS value is ignored.
docker run -d --name miso-gallery \
-p 5000:5000 \
-v /path/to/images:/data \
-e SECRET_KEY=your-session-secret \
-e ADMIN_PASSWORD=your-password \
-e LLM_READ_API_KEYS=gallery-read-key \
-e LLM_WRITE_API_KEYS=gallery-write-key \
ghcr.io/misospace/miso-gallery:latestAuthenticate each request with a Bearer token:
curl -H "Authorization: Bearer gallery-read-key" \
http://localhost:5000/api/llm/imagesLLM API endpoints are token-authenticated and do not require CSRF tokens. Existing browser/session authentication continues to work for the UI.
| Method | Path | Description |
|---|---|---|
GET |
/api/llm/images?q=<query> |
Recursively list/search media. q matches filenames or relative paths. |
GET |
/api/llm/image/<relpath> |
Return metadata for a single image/video. |
GET |
/api/llm/recent?limit=N |
Return recent media sorted by modification time. Default 50, max 500. |
GET |
/api/llm/folders |
Return folder listing with relative paths and parent folders. |
Example:
curl -H "Authorization: Bearer gallery-read-key" \
"http://localhost:5000/api/llm/images?q=cat"Response shape:
{
"count": 1,
"images": [
{
"name": "cat.jpg",
"rel_path": "cats/cat.jpg",
"media_type": "image",
"size": 12345,
"size_human": "12.1 KB",
"modified": "2026-04-28T12:34:56Z",
"mtime": 1777398896.0,
"view_url": "/view/cats/cat.jpg",
"thumb_url": "/thumb/cats/cat.jpg"
}
]
}| Method | Path | Body | Description |
|---|---|---|---|
POST |
/api/llm/tags |
{"rel_path":"cats/cat.jpg","tag":"favorite","action":"add"} |
Add/remove tags. Tag persistence is currently log-only. |
POST |
/api/llm/delete |
{"rel_path":"cats/cat.jpg"} |
Move one media file to trash and clear its thumbnail cache. |
POST |
/api/llm/bulk-delete |
{"rel_paths":["a.jpg","b.jpg"]} |
Move multiple media files to trash. |
POST |
/api/llm/dedup |
{} or {"remove":true} |
Find duplicate media by SHA-256. Defaults to dry-run; remove:true moves duplicates to trash. |
Delete example:
curl -X POST \
-H "Authorization: Bearer gallery-write-key" \
-H "Content-Type: application/json" \
-d '{"rel_path":"cats/cat.jpg"}' \
http://localhost:5000/api/llm/deleteDedup dry-run example:
curl -X POST \
-H "Authorization: Bearer gallery-write-key" \
-H "Content-Type: application/json" \
-d '{}' \
http://localhost:5000/api/llm/dedupMost LLM integrations do not need task execution. Use the gallery-management endpoints above unless you intentionally want Miso Gallery to expose a small set of preconfigured server-side automation commands.
Task execution is an optional advanced feature that reuses the existing webhook task infrastructure. It is disabled unless WEBHOOK_ENABLED=true, and only commands explicitly configured through WEBHOOK_TASK_* environment variables can be run. This can be useful for trusted maintenance or generation scripts that should run on the gallery host, but it is not required for normal LLM-to-gallery interaction.
docker run -d --name miso-gallery \
-p 5000:5000 \
-v /path/to/images:/data \
-e SECRET_KEY=your-session-secret \
-e LLM_READ_API_KEYS=gallery-read-key \
-e LLM_WRITE_API_KEYS=gallery-write-key \
-e WEBHOOK_ENABLED=true \
-e 'WEBHOOK_TASK_GENERATE=python3 /data/scripts/generate.py {params.prompt}' \
ghcr.io/misospace/miso-gallery:latestRun a configured task:
curl -X POST \
-H "Authorization: Bearer gallery-write-key" \
-H "Content-Type: application/json" \
-d '{"task":"generate","params":{"prompt":"a cozy bowl of miso soup"}}' \
http://localhost:5000/api/llm/task/runTask execution requires a write-scoped LLM API key. Read-scoped keys can browse gallery metadata but cannot run configured server-side tasks. Task commands run from DATA_FOLDER, scalar params are shell-quoted, and the timeout is controlled by WEBHOOK_TASK_TIMEOUT with a default of 30 seconds.
# Clone and setup
git clone https://github.com/misospace/miso-gallery.git
cd miso-gallery
# Create virtual environment
python3 -m venv venv
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Run development server
python app.py# Build Docker image
docker build -t miso-gallery:latest .
# Run locally
docker run -p 5000:5000 -v ./images:/data miso-gallery:latestUse the Manual Release GitHub Actions workflow and enter a version like 0.4.6. It normalizes v0.4.6 to 0.4.6, updates the in-app version string in app.py, pushes that bump to main through the configured bot identity, creates the plain-semver tag, and creates the GitHub release with generated notes.
The Build workflow (triggered by a published release) validates that APP_VERSION in app.py matches the release tag before building Docker images. If they diverge, the build fails to prevent releasing a binary with an incorrect version string.
MIT