config: env-driven CORS, ports, log level, timezone

The CORS allowed-origins list, host port mappings, log level, container
timezone, and worker concurrency are now all driven by environment
variables with sane defaults. Same-origin access through the nginx
proxy keeps working with no config; direct cross-origin backend
access can be locked down via ALLOWED_ORIGINS.

- backend/config: ALLOWED_ORIGINS env (comma-separated, "*" for any)
  exposed via settings.cors_origins. LOG_LEVEL too.
- backend/main: build the CORS middleware from settings.cors_origins,
  auto-disable allow_credentials when origins is wildcard (CORS spec
  forbids credentials + "*").
- docker-compose: parameterize FRONTEND_PORT, BACKEND_PORT, REDIS_PORT,
  CELERYD_CONCURRENCY, LOG_LEVEL, and TZ via ${VAR:-default} so each
  has a working fallback if the .env entry is missing.
- .env.example: new template documenting every knob with examples.
- .env: pruned to only the values that diverge from .env.example;
  removed dead VITE_API_URL.
- README: configuration knobs table + "accessing from another machine"
  section explaining the same-origin proxy story.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 20:31:59 +02:00
parent ac5b18b60c
commit fc63b1f69d
6 changed files with 189 additions and 49 deletions

40
.env
View File

@@ -1,30 +1,20 @@
# Environment variables for Mulita
#
# Set PHOTO_DIRS to the HOST path of your photo library. The compose file
# mounts this at /photos inside the container, and on first boot Mulita
# auto-creates a source root pointing at /photos so your library is
# scanned with zero further configuration.
#
# Examples:
# macOS / Linux: PHOTO_DIRS=/Users/you/Pictures
# Network share: PHOTO_DIRS=/mnt/nas/photos
# Windows (WSL): PHOTO_DIRS=/mnt/c/Users/you/Pictures
# Mulita / PhotoVault local environment.
# See .env.example for the full list of knobs and their docs.
# REQUIRED — host path to your photo library.
PHOTO_DIRS=/Users/dtoro/Pictures/MulitaTest
# Redis configuration
REDIS_URL=redis://localhost:6379
# Ports — change if 3000 / 8001 collide with other services on the host.
FRONTEND_PORT=3000
BACKEND_PORT=8001
REDIS_PORT=6379
# Database URL
DATABASE_URL=sqlite+aiosqlite:///data/db/mulita.db
# CORS — wildcard for local dev. Lock down for real deployments.
ALLOWED_ORIGINS=*
# Celery configuration
CELERY_BROKER_URL=redis://localhost:6379
CELERY_RESULT_BACKEND=redis://localhost:6379
# Logging + timezone.
LOG_LEVEL=INFO
TZ=UTC
# Celery worker pool.
CELERYD_CONCURRENCY=4
# API settings
API_HOST=0.0.0.0
API_PORT=8000
# Frontend settings
VITE_API_URL=http://localhost:8000

83
.env.example Normal file
View File

@@ -0,0 +1,83 @@
# ─────────────────────────────────────────────────────────────────────────────
# Mulita / PhotoVault — example environment file
#
# Copy this file to `.env` and adjust the values for your setup. Every key
# below has a sensible default in docker-compose.yml, so you only need to
# uncomment the ones you actually want to change.
# ─────────────────────────────────────────────────────────────────────────────
# ── REQUIRED ─────────────────────────────────────────────────────────────────
# Host path to your photo library. The compose file mounts this at /photos
# inside the backend + worker containers. The backend creates a default
# source root pointing at /photos on first boot, so once this is set the
# library is scanned with zero further configuration.
#
# Examples:
# macOS / Linux: PHOTO_DIRS=/Users/you/Pictures
# Network share: PHOTO_DIRS=/mnt/nas/photos
# Windows (WSL): PHOTO_DIRS=/mnt/c/Users/you/Pictures
PHOTO_DIRS=./photos
# ── PORTS ────────────────────────────────────────────────────────────────────
# Host port the SPA is served on. Browse to http://<host>:<FRONTEND_PORT>/.
FRONTEND_PORT=3000
# Host port for the backend API. Almost never needed directly — the frontend
# nginx proxies /api/ to the backend over the internal compose network. Kept
# exposed for debugging / curl.
BACKEND_PORT=8001
# Redis host port. Internal services reach Redis on its container name; this
# is just for local debugging.
REDIS_PORT=6379
# ── CORS ─────────────────────────────────────────────────────────────────────
# Comma-separated list of allowed origins for direct browser access to the
# backend. Same-origin requests through the nginx / vite proxy never trip
# CORS, so this only matters when something hits the backend port directly
# from a different origin (e.g. another machine, dev tools, a reverse proxy
# under a different hostname).
#
# Default "*" is permissive, fine for a single-user homelab. Lock it down in
# real deployments:
# ALLOWED_ORIGINS=https://photos.example.com
# ALLOWED_ORIGINS=https://photos.example.com,http://192.168.1.10:3000
ALLOWED_ORIGINS=*
# ── LOGGING / TIMEZONE ───────────────────────────────────────────────────────
# Python log level for the backend and Celery worker. Bump to DEBUG when
# chasing scan / thumbnail issues.
LOG_LEVEL=INFO
# Container timezone. Affects the timestamps in logs and the "added at"
# field on newly imported photos. Defaults to UTC.
# TZ=Europe/Berlin
# TZ=America/New_York
TZ=UTC
# ── WORKER CONCURRENCY ───────────────────────────────────────────────────────
# How many parallel Celery worker processes to spin up. Each one can run
# one scan / thumbnail / metadata job at a time. Bump on a beefy host with a
# big library; lower on a Pi.
CELERYD_CONCURRENCY=4
# ── INTERNAL (rarely overridden) ─────────────────────────────────────────────
# These point at the in-compose Redis and the bind-mounted SQLite db. Override
# only if you're running Mulita without docker-compose or against an external
# Redis.
# REDIS_URL=redis://redis:6379
# CELERY_BROKER_URL=redis://redis:6379
# CELERY_RESULT_BACKEND=redis://redis:6379
# DATABASE_URL=sqlite+aiosqlite:////data/db/mulita.db

View File

@@ -43,19 +43,16 @@ git clone <repository-url>
cd muleimage
```
2. Set **one** environment variable in `.env` — the **host** directory
that contains your photo library. Whatever you point at will become
your library inside Mulita.
2. Copy the example env file and set **one** variable — the **host**
directory that contains your photo library. Whatever you point at
will become your library inside Mulita.
```bash
# macOS / Linux
PHOTO_DIRS=/Users/you/Pictures
# or any folder
PHOTO_DIRS=/mnt/nas/photos
# Windows (WSL)
PHOTO_DIRS=/mnt/c/Users/you/Pictures
cp .env.example .env
# then edit .env and set PHOTO_DIRS:
# macOS / Linux: PHOTO_DIRS=/Users/you/Pictures
# Network share: PHOTO_DIRS=/mnt/nas/photos
# Windows (WSL): PHOTO_DIRS=/mnt/c/Users/you/Pictures
```
3. Start the stack:
@@ -70,6 +67,38 @@ docker compose up -d
You don't need to touch `mulita.yml` or the API to get started.
### Configuration knobs
Everything is environment-driven. `PHOTO_DIRS` is the only required
value; the rest have sensible defaults documented in `.env.example`:
| Variable | Default | Notes |
|----------------------|---------|----------------------------------------------------|
| `PHOTO_DIRS` | — | **Required.** Host path mounted at `/photos`. |
| `FRONTEND_PORT` | `3000` | SPA host port. Bump if `3000` is taken. |
| `BACKEND_PORT` | `8001` | Direct backend port (debug only — frontend uses internal nginx proxy). |
| `REDIS_PORT` | `6379` | Redis host port (internal services don't need it). |
| `ALLOWED_ORIGINS` | `*` | Comma-separated CORS origins for direct backend access. Lock down for prod, e.g. `https://photos.example.com`. |
| `LOG_LEVEL` | `INFO` | Backend + worker log level. `DEBUG` for chasing scan issues. |
| `TZ` | `UTC` | Container timezone. Affects log timestamps and "added at". |
| `CELERYD_CONCURRENCY`| `4` | Parallel worker processes (scans, thumbs, metadata). Lower on a Pi, higher on a beefy host. |
### Accessing from another machine
The frontend talks to the backend through its bundled nginx, which
proxies `/api/` to the backend on the internal compose network. That
means requests are always **same-origin** as the page, so accessing
Mulita from another host works without any CORS dance:
```
http://<your-server-ip>:3000
```
If you want to put it behind a reverse proxy at e.g.
`https://photos.your.tld`, set `ALLOWED_ORIGINS` to that host so the
backend's direct port (`BACKEND_PORT`) also accepts cross-origin
requests if anything bypasses the proxy.
### How libraries are managed
Mulita is **config-driven**: the host directory you mount via

View File

@@ -70,7 +70,29 @@ class Settings(BaseSettings):
# API settings
api_host: str = Field(default="0.0.0.0", env="API_HOST")
api_port: int = Field(default=8000, env="API_PORT")
# CORS — comma-separated list of allowed origins, or "*" for any.
# Same-origin requests (the normal case behind nginx / vite proxy)
# never trip CORS, so this is only for direct browser access from
# other origins (LAN IP, reverse proxy, dev tools).
allowed_origins: str = Field(default="*", env="ALLOWED_ORIGINS")
# Logging — accepts standard python levels (DEBUG, INFO, WARNING,
# ERROR, CRITICAL). Bumped from INFO when chasing a problem.
log_level: str = Field(default="INFO", env="LOG_LEVEL")
@property
def cors_origins(self) -> list[str]:
"""Parse the ALLOWED_ORIGINS env var into a list. Accepts:
- "*" → wildcard (single-element list ["*"])
- "http://a.com,http://b.com" → split + strip
Empty entries are dropped.
"""
raw = (self.allowed_origins or "").strip()
if not raw or raw == "*":
return ["*"]
return [o.strip() for o in raw.split(",") if o.strip()]
# App configuration from YAML
_config: Optional[MulitaConfig] = None

View File

@@ -64,14 +64,18 @@ app = FastAPI(
# Configure CORS. The frontend normally talks to the backend through the
# nginx (prod) or vite (dev) proxy, so requests are same-origin and never
# trip CORS. The wildcard here is a fallback for the rare case where a
# user / script hits the backend directly from a browser at some other
# origin (LAN IP, reverse proxy under a different host, etc.). This is a
# single-user homelab tool, so a permissive CORS policy is fine.
# trip CORS. ALLOWED_ORIGINS in .env controls the fallback for direct
# browser access from other origins (LAN IP, reverse proxy under a
# different host). Defaults to "*" since this is a single-user homelab
# tool; lock it down by setting e.g. ALLOWED_ORIGINS=https://photos.your.tld
# in production deployments.
_origins = settings.cors_origins
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_origins=_origins,
# Wildcard origins can't be combined with credentials per the CORS
# spec, so credentials get auto-disabled in that case.
allow_credentials=_origins != ["*"],
allow_methods=["*"],
allow_headers=["*"],
)

View File

@@ -2,12 +2,14 @@ version: '3.8'
services:
frontend:
build:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: mulita-frontend
ports:
- "3000:80"
# Host port is configurable via FRONTEND_PORT in .env so multiple
# instances / other services on the same host don't collide.
- "${FRONTEND_PORT:-3000}:80"
depends_on:
- backend
networks:
@@ -20,7 +22,10 @@ services:
dockerfile: Dockerfile
container_name: mulita-backend
ports:
- "8001:8000"
# Direct backend access on the host is rarely needed (the frontend
# talks to it through the nginx /api proxy on the same network),
# but it's exposed for debugging / curl. Override with BACKEND_PORT.
- "${BACKEND_PORT:-8001}:8000"
volumes:
- ./mulita.yml:/app/config/mulita.yml:ro
# The single host → container mount for your photo library. Set
@@ -38,6 +43,9 @@ services:
- CELERY_BROKER_URL=redis://redis:6379
- CELERY_RESULT_BACKEND=redis://redis:6379
- PHOTO_DIRS=${PHOTO_DIRS:-/photos}
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-*}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- TZ=${TZ:-UTC}
depends_on:
- redis
networks:
@@ -49,7 +57,7 @@ services:
context: ./backend
dockerfile: Dockerfile
container_name: mulita-worker
command: celery -A app.tasks.celery worker --loglevel=info --concurrency=4
command: celery -A app.tasks.celery worker --loglevel=${LOG_LEVEL:-info} --concurrency=${CELERYD_CONCURRENCY:-4}
volumes:
- ./mulita.yml:/app/config/mulita.yml:ro
- ${PHOTO_DIRS:-./photos}:/photos:rw
@@ -62,7 +70,9 @@ services:
- CELERY_BROKER_URL=redis://redis:6379
- CELERY_RESULT_BACKEND=redis://redis:6379
- PHOTO_DIRS=${PHOTO_DIRS:-/photos}
- CELERYD_CONCURRENCY=4
- CELERYD_CONCURRENCY=${CELERYD_CONCURRENCY:-4}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- TZ=${TZ:-UTC}
depends_on:
- redis
- backend
@@ -73,8 +83,10 @@ services:
redis:
image: redis:7-alpine
container_name: mulita-redis
# Host port exposed only for local debugging; the backend / worker
# reach Redis via the internal mulita-network on its container name.
ports:
- "6379:6379"
- "${REDIS_PORT:-6379}:6379"
volumes:
- redis_data:/data
networks: