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:
40
.env
40
.env
@@ -1,30 +1,20 @@
|
|||||||
# Environment variables for Mulita
|
# Mulita / PhotoVault local environment.
|
||||||
#
|
# See .env.example for the full list of knobs and their docs.
|
||||||
# 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
|
# REQUIRED — host path to your photo library.
|
||||||
# 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
|
|
||||||
PHOTO_DIRS=/Users/dtoro/Pictures/MulitaTest
|
PHOTO_DIRS=/Users/dtoro/Pictures/MulitaTest
|
||||||
|
|
||||||
# Redis configuration
|
# Ports — change if 3000 / 8001 collide with other services on the host.
|
||||||
REDIS_URL=redis://localhost:6379
|
FRONTEND_PORT=3000
|
||||||
|
BACKEND_PORT=8001
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
# Database URL
|
# CORS — wildcard for local dev. Lock down for real deployments.
|
||||||
DATABASE_URL=sqlite+aiosqlite:///data/db/mulita.db
|
ALLOWED_ORIGINS=*
|
||||||
|
|
||||||
# Celery configuration
|
# Logging + timezone.
|
||||||
CELERY_BROKER_URL=redis://localhost:6379
|
LOG_LEVEL=INFO
|
||||||
CELERY_RESULT_BACKEND=redis://localhost:6379
|
TZ=UTC
|
||||||
|
|
||||||
|
# Celery worker pool.
|
||||||
CELERYD_CONCURRENCY=4
|
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
83
.env.example
Normal 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
|
||||||
51
README.md
51
README.md
@@ -43,19 +43,16 @@ git clone <repository-url>
|
|||||||
cd muleimage
|
cd muleimage
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Set **one** environment variable in `.env` — the **host** directory
|
2. Copy the example env file and set **one** variable — the **host**
|
||||||
that contains your photo library. Whatever you point at will become
|
directory that contains your photo library. Whatever you point at
|
||||||
your library inside Mulita.
|
will become your library inside Mulita.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS / Linux
|
cp .env.example .env
|
||||||
PHOTO_DIRS=/Users/you/Pictures
|
# then edit .env and set PHOTO_DIRS:
|
||||||
|
# macOS / Linux: PHOTO_DIRS=/Users/you/Pictures
|
||||||
# or any folder
|
# Network share: PHOTO_DIRS=/mnt/nas/photos
|
||||||
PHOTO_DIRS=/mnt/nas/photos
|
# Windows (WSL): PHOTO_DIRS=/mnt/c/Users/you/Pictures
|
||||||
|
|
||||||
# Windows (WSL)
|
|
||||||
PHOTO_DIRS=/mnt/c/Users/you/Pictures
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Start the stack:
|
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.
|
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
|
### How libraries are managed
|
||||||
|
|
||||||
Mulita is **config-driven**: the host directory you mount via
|
Mulita is **config-driven**: the host directory you mount via
|
||||||
|
|||||||
@@ -70,7 +70,29 @@ class Settings(BaseSettings):
|
|||||||
# API settings
|
# API settings
|
||||||
api_host: str = Field(default="0.0.0.0", env="API_HOST")
|
api_host: str = Field(default="0.0.0.0", env="API_HOST")
|
||||||
api_port: int = Field(default=8000, env="API_PORT")
|
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
|
# App configuration from YAML
|
||||||
_config: Optional[MulitaConfig] = None
|
_config: Optional[MulitaConfig] = None
|
||||||
|
|
||||||
|
|||||||
@@ -64,14 +64,18 @@ app = FastAPI(
|
|||||||
|
|
||||||
# Configure CORS. The frontend normally talks to the backend through the
|
# Configure CORS. The frontend normally talks to the backend through the
|
||||||
# nginx (prod) or vite (dev) proxy, so requests are same-origin and never
|
# 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
|
# trip CORS. ALLOWED_ORIGINS in .env controls the fallback for direct
|
||||||
# user / script hits the backend directly from a browser at some other
|
# browser access from other origins (LAN IP, reverse proxy under a
|
||||||
# origin (LAN IP, reverse proxy under a different host, etc.). This is a
|
# different host). Defaults to "*" since this is a single-user homelab
|
||||||
# single-user homelab tool, so a permissive CORS policy is fine.
|
# tool; lock it down by setting e.g. ALLOWED_ORIGINS=https://photos.your.tld
|
||||||
|
# in production deployments.
|
||||||
|
_origins = settings.cors_origins
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=_origins,
|
||||||
allow_credentials=False,
|
# 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_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ version: '3.8'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: mulita-frontend
|
container_name: mulita-frontend
|
||||||
ports:
|
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:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
networks:
|
networks:
|
||||||
@@ -20,7 +22,10 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: mulita-backend
|
container_name: mulita-backend
|
||||||
ports:
|
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:
|
volumes:
|
||||||
- ./mulita.yml:/app/config/mulita.yml:ro
|
- ./mulita.yml:/app/config/mulita.yml:ro
|
||||||
# The single host → container mount for your photo library. Set
|
# The single host → container mount for your photo library. Set
|
||||||
@@ -38,6 +43,9 @@ services:
|
|||||||
- CELERY_BROKER_URL=redis://redis:6379
|
- CELERY_BROKER_URL=redis://redis:6379
|
||||||
- CELERY_RESULT_BACKEND=redis://redis:6379
|
- CELERY_RESULT_BACKEND=redis://redis:6379
|
||||||
- PHOTO_DIRS=${PHOTO_DIRS:-/photos}
|
- PHOTO_DIRS=${PHOTO_DIRS:-/photos}
|
||||||
|
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-*}
|
||||||
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||||
|
- TZ=${TZ:-UTC}
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
networks:
|
networks:
|
||||||
@@ -49,7 +57,7 @@ services:
|
|||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: mulita-worker
|
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:
|
volumes:
|
||||||
- ./mulita.yml:/app/config/mulita.yml:ro
|
- ./mulita.yml:/app/config/mulita.yml:ro
|
||||||
- ${PHOTO_DIRS:-./photos}:/photos:rw
|
- ${PHOTO_DIRS:-./photos}:/photos:rw
|
||||||
@@ -62,7 +70,9 @@ services:
|
|||||||
- CELERY_BROKER_URL=redis://redis:6379
|
- CELERY_BROKER_URL=redis://redis:6379
|
||||||
- CELERY_RESULT_BACKEND=redis://redis:6379
|
- CELERY_RESULT_BACKEND=redis://redis:6379
|
||||||
- PHOTO_DIRS=${PHOTO_DIRS:-/photos}
|
- PHOTO_DIRS=${PHOTO_DIRS:-/photos}
|
||||||
- CELERYD_CONCURRENCY=4
|
- CELERYD_CONCURRENCY=${CELERYD_CONCURRENCY:-4}
|
||||||
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||||
|
- TZ=${TZ:-UTC}
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- backend
|
- backend
|
||||||
@@ -73,8 +83,10 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: mulita-redis
|
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:
|
ports:
|
||||||
- "6379:6379"
|
- "${REDIS_PORT:-6379}:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
Reference in New Issue
Block a user