Configuration
Every environment variable the stack reads, what it changes, and its default. The
authoritative source is src/metaedit/config.py
(Settings, via pydantic-settings — case-insensitive, .env is read automatically);
this page is for reading, not for code generation. .env.example is
the template to copy and carries comments on the variables you are most likely to
touch.
Values are environment only — nothing is baked into the image and no secrets are
logged (both keys are SecretStr, and a test asserts they cannot leak through
model_dump() or repr()).
Compose-level variables
These are interpolated by docker-compose.yml when bringing
the stack up; they shape the deployment rather than the app process.
| Variable |
Default |
Meaning |
APP_BIND |
127.0.0.1 |
Host address the app port is published on. Loopback on purpose — see the security note below. |
APP_PORT |
8080 |
Host port for the app. The container always listens on 8080. |
POSTGRES_USER |
metaedit |
Postgres superuser name, also the database owner. |
POSTGRES_PASSWORD |
metaedit |
Postgres password. Set this — the default is a placeholder. |
POSTGRES_DB |
metaedit |
Database name. |
POSTGRES_PORT |
5432 |
Host port for Postgres, published for local tooling. |
The compose file also passes JELLYFIN_URL, JELLYFIN_API_KEY, LASTFM_API_KEY,
LASTFM_USER_AGENT, ARCHIVE_SOFT_CAP_BYTES, LASTFM_MAX_RPS, LOG_LEVEL and
LOG_JSON through to the app container; their meaning is in the app tables below. A
migrate one-shot service runs alembic upgrade head before the app starts, so
migrations never run implicitly at application startup.
App: HTTP server and logging
| Variable |
Default |
Meaning |
BIND_HOST |
127.0.0.1 |
Address the API binds inside the container. |
BIND_PORT |
8080 |
Port the API binds inside the container. |
LOG_LEVEL |
INFO |
Python log level. |
LOG_JSON |
true |
Structured JSON logs (structlog). Turn off for human-readable local logs. |
Logs go to stderr — the CLI keeps stdout parseable for its JSON reports.
App: Jellyfin
| Variable |
Default |
Meaning |
JELLYFIN_URL |
http://localhost:8096 |
Server root. A bare host:port is accepted and normalised to http://, because that is the natural way to write a LAN address. |
JELLYFIN_API_KEY |
(empty) |
Administrator API key. Writes are gated on RequiresElevation; a non-admin key can browse but not save, and GET /api/info reports which one you have. |
JELLYFIN_USER_ID |
(empty) |
An API key carries no user identity, and Jellyfin’s single-item endpoint requires a user context. When empty, a user is discovered from /Users (first administrator in sorted-id order, cached per process); set it to pin the choice on a server where “first” is not the right one. |
JELLYFIN_TIMEOUT_S |
10.0 |
Per-request timeout, seconds. |
JELLYFIN_MAX_RETRIES |
2 |
Retries on transient failures. |
App: Last.fm
| Variable |
Default |
Meaning |
LASTFM_API_KEY |
(empty) |
API key. Read-only usage: no user auth, no session signing, nothing is ever written back to Last.fm. |
LASTFM_USER_AGENT |
metaedit/0.2.0 (+https://github.com/sredevopsorg/metaedit) |
Identification Last.fm asks for. Leave the line out entirely for the default; a set-but-empty value sends no User-Agent, which Last.fm may treat as abusive. |
LASTFM_TIMEOUT_S |
10.0 |
Per-request timeout, seconds. |
LASTFM_MAX_RPS |
4.0 |
Shared client-side rate limit, requests per second. |
LASTFM_BURST |
8 |
Burst allowance for the rate limiter. |
LASTFM_MAX_RETRIES |
3 |
Retries with backoff. |
App: database
| Variable |
Default |
Meaning |
DATABASE_URL |
postgresql+psycopg://metaedit:metaedit@localhost:5432/metaedit |
SQLAlchemy URL. Compose points it at the postgres service; local development uses the host-published port. |
DB_POOL_SIZE |
5 |
Connection pool size. |
DB_MAX_OVERFLOW |
5 |
Pool overflow allowance. |
DB_ECHO |
false |
Echo SQL — debugging only. |
App: archive
| Variable |
Default |
Meaning |
ARCHIVE_ENABLED |
true |
Master switch. false stops recording and skips partition maintenance at startup; the editor then finds no candidates. Escape hatch, not a supported configuration. |
ARCHIVE_LOG_REQUESTS |
true |
Log every HTTP attempt in lastfm_request. |
ARCHIVE_SOFT_CAP_BYTES |
104857600 |
The Last.fm ToS “Reasonable Usage Cap” (100 MiB). Soft: re-observing a stored body costs nothing; only genuinely new payloads count against it. |
ARCHIVE_WARN_RATIO |
0.8 |
Warn when stored bytes cross this fraction of the cap. |
ARCHIVE_REFUSE_RATIO |
1.0 |
Refuse new payloads (ArchiveCapReached) at this fraction. Must not be below the warn ratio — the app rejects that at startup, because a warning that cannot fire before the refusal is dead code. |
ARCHIVE_FRESHNESS_TTL_S |
86400 |
How long a stored response may be served without a refresh (24 h). |
ARCHIVE_SIMILAR_FRESHNESS_TTL_S |
604800 |
Same, for artist.getSimilar — it changes slowly (7 days). |
ARCHIVE_PARTITION_MONTHS_AHEAD |
3 |
How many monthly lastfm_request partitions metaedit partitions pre-creates by default. |
App: mapping policy
| Variable |
Default |
Meaning |
LASTFM_MIN_TAG_COUNT |
0 |
Tags below this getTopTags count are not proposed. |
LASTFM_GENRE_LIMIT |
5 |
How many genres mapping proposes at most. |
LASTFM_STYLE_LIMIT |
10 |
How many styles (mapped onto Tags) at most. |
MAX_TAGS_PER_ITEM |
30 |
Hard ceiling on tags written per item. |
MAX_TAG_LENGTH |
100 |
Maximum tag length accepted. |
TAG_BLACKLIST_EXTRA |
(empty) |
Comma-separated genres never proposed — deployment policy, merged with the built-in list and the database list edited at Settings → Blacklisted genres. Matching is exact and case-insensitive; it never rewrites existing items. |
App: frontend
| Variable |
Default |
Meaning |
WEB_DIST_DIR |
(empty) |
Where the built SPA is served from. Empty disables SPA serving (API only); the compose image sets /app/web. |
Read directly, outside Settings
| Variable |
Default |
Meaning |
METAEDIT_BROWSE_SCAN_CAP |
2000 |
Bounds how many items a filter that needs a read will scan (missing, exclude). Raise it on a large library and filtering gets slower, not wrong. It began as a test hook for the truncation path; it is read per call, not cached. |
Tests only
| Variable |
Default |
Meaning |
METAEDIT_TEST_DATABASE_URL |
postgresql+psycopg://metaedit:metaedit@localhost:5432/metaedit |
Admin URL for the integration suite, which builds disposable per-test databases from a template migrated once per session. Tests skip cleanly when no Postgres 18 answers. |
Security note
APP_BIND defaults to loopback deliberately: the app holds an administrator Jellyfin
key and has no multi-user authentication of its own. If you expose it, put an
authenticating proxy in front of it first. The app adds its own hardening — CSP,
nosniff, frame-ancestors 'none', a 2 MiB body cap — but that is defence in depth,
not an authentication layer. See API.