Configuration
Everything is a Spring property and every property is overridable via environment variables
(relaxed binding: app.ister.server.name ⇒ APP_ISTER_SERVER_NAME). Many settings also have a
dedicated short env var, listed below. Defaults are sensible for a single-node home setup.
Core services
| Setting | Env var | Default | Notes |
|---|---|---|---|
| Database host / port / name | DB_HOST / DB_PATH / DB_NAME | localhost / 5432 / ister | PostgreSQL |
| Database credentials | DB_USER / DB_PASSWORD | ister / ister | change in production |
| Connection pool | DB_POOL_SIZE | 20 | playback fans out to many concurrent queries |
| Connection timeout (ms) | DB_CONNECTION_TIMEOUT | 10000 | how long a thread waits for a pooled connection before failing |
| RabbitMQ | SPRING_RABBITMQ_HOST / _PORT / _USERNAME / _PASSWORD | localhost, 5672, user/password | port defaults to the RabbitMQ default 5672 |
| OIDC issuer | OIDC_URL | http://keycloak:8060/realms/Ister | Keycloak-compatible; the default matches the bundled dev Keycloak. Production: e.g. https://keycloak.example.com/realms/Home |
| OIDC audience | OIDC_AUDIENCE | (empty) | Optional. When set, an access token must list it in aud or carry it as azp (the client id, e.g. ister). Empty = issuer and signature only, so tokens for other clients in the same realm are accepted too. |
Identity and paths
| Setting | Env var | Default | Notes |
|---|---|---|---|
app.ister.server.name | APP_ISTER_SERVER_NAME | Test server | must be unique per node |
app.ister.server.url | APP_ISTER_SERVER_URL | http://localhost:8080 | how clients and other nodes reach this node |
app.ister.cluster.name | APP_ISTER_CLUSTER_NAME | the server name | same value on every node of a cluster |
| Cache directory | CACHE_DIR | /cache/ | images, podcast downloads |
| Temp directory | TMP_DIR | /tmp/ister/ | HLS transcode output |
app.ister.server.websocket.allowed-origins | APP_ISTER_SERVER_WEBSOCKET_ALLOWED_ORIGINS | empty | extra origins allowed on the GraphQL websocket handshake. The public host from app.ister.server.url is allowed automatically (both schemes, any port), so you only need this when the server is served under a second name. * allows any origin. |
Object storage (S3)
See Object storage. Connections are indexed lists (APP_ISTER_S3_CONNECTIONS_0_NAME, …).
| Setting | Env var | Default | Notes |
|---|---|---|---|
app.ister.s3.connections[n].name | referenced by directories and the shared stores | ||
app.ister.s3.connections[n].endpoint | empty | empty = AWS; http://garage:3900 for a self-hosted server | |
app.ister.s3.connections[n].region | us-east-1 | ||
app.ister.s3.connections[n].bucket | one bucket per connection | ||
app.ister.s3.connections[n].path-style | true | path-style addressing, needed by MinIO/Garage/Ceph | |
app.ister.s3.connections[n].access-key / .secret-key | empty | empty = the SDK's default credential chain (AWS) | |
app.ister.disk.directories[n].s3-connection / .prefix | makes the directory an S3 directory (no path); prefix optional | ||
app.ister.s3.ffmpeg-direct | S3_FFMPEG_DIRECT | false | true: ffmpeg reads presigned S3 URLs instead of the node's own proxy |
app.ister.s3.presign-ttl | S3_PRESIGN_TTL | 2h | lifetime of those URLs; keep above the longest pass |
app.ister.server.self-url | SELF_URL | http://127.0.0.1:<server.port> | where the node reaches its own proxy for ffmpeg; keep it on loopback behind a gateway with request timeouts |
app.ister.s3.local-copy-dir / .local-copy-max-bytes | S3_LOCAL_COPY_DIR / S3_LOCAL_COPY_MAX_BYTES | <tmp-dir>/s3-scratch/ / 2 GiB | LRU cache of objects that must be read as a local file (epub/cbz/pdf, OCR) |
app.ister.server.cache-s3-connection / .cache-s3-prefix | CACHE_S3_CONNECTION / CACHE_S3_PREFIX | empty / cache | the cluster-shared cache; replaces CACHE_DIR for new derived files |
app.ister.server.tmp-s3-connection / .tmp-s3-prefix | TMP_S3_CONNECTION / TMP_S3_PREFIX | empty / tmp | the cluster-shared transcode store; TMP_DIR stays the local encoding scratch |
Metadata and languages
| Setting | Env var | Default | Notes |
|---|---|---|---|
app.ister.server.TMDB.apikey | APP_ISTER_SERVER_TMDB_APIKEY | unset | TMDB API read access token. Without it, movie/show metadata fetching is skipped — you get bare filenames. |
app.ister.server.TMDB.max-requests-per-second | 30 | stays under TMDB's ~40 rps limit | |
app.ister.worker.tmdb.certification-country | US | ISO 3166-1 country whose certification/content rating (e.g. 16, PG-13, TV-MA) is stored on movies and shows; falls back to US, then any country that has one. | |
app.ister.languages | ISTER_LANGUAGES | en,nl | comma-separated ISO-639-1 tags; first = primary. Drives which languages metadata is fetched in and which languages search indexes. Changing it requires a re-scan plus rebuildSearchIndex — see Search. |
app.ister.worker.musicbrainz.min-request-interval-millis | 1100 | spacing between MusicBrainz calls; stays just above their 1-request-per-second policy — lower it only against your own mirror | |
app.ister.worker.openlibrary.min-request-interval-millis | 1000 | spacing between Open Library calls (search, author, work lookups) | |
app.ister.worker.wikipedia.min-request-interval-millis | 500 | spacing between Wikidata/Wikipedia calls (person bios, series lookups) |
Search (Typesense)
| Env var | Default |
|---|---|
TYPESENSE_ENABLED | false |
TYPESENSE_HOST / TYPESENSE_PORT / TYPESENSE_PROTOCOL | localhost / 8108 / http |
TYPESENSE_API_KEY / TYPESENSE_COLLECTION | empty / media |
See Search for the enable/reindex procedure.
Transcoding
| Setting | Env var | Default | Notes |
|---|---|---|---|
| FFmpeg directory | FFMPEG_DIR | /usr/bin | directory holding ffmpeg/ffprobe |
| mkvextract | MKVEXTRACT | /usr/bin/mkvextract | unpacks DVD (VobSub) subtitle tracks for the bitmap-subtitle sprite sheets; Blu-ray (PGS) needs only FFmpeg |
app.ister.transcoder.hls.hwaccel | HLS_HWACCEL | none | vaapi (Intel/AMD) or nvdec (NVIDIA); the compose file shows the required device mappings |
app.ister.transcoder.hls.hwaccel-device | HLS_HWACCEL_DEVICE | /dev/dri/renderD128 | VAAPI only |
app.ister.transcoder.hls.max-concurrent-files | HLS_MAX_CONCURRENT_FILES | 2 | files transcoded simultaneously; pre-transcoding shares this budget |
Advanced transcoding
Rarely touched — the defaults suit most setups. These have no dedicated short env var (use relaxed
binding, e.g. APP_ISTER_TRANSCODER_HLS_MAX_BACKGROUND_FILES). The mechanics behind them are in the
architecture guide, Transcoding.
| Property | Default | Notes |
|---|---|---|
app.ister.transcoder.hls.max-concurrent-passes | 4 | FFmpeg passes running at once (thread pool size) |
app.ister.transcoder.hls.max-background-files | 1 | files a background pre-transcode may hold, kept below the interactive budget |
app.ister.transcoder.hls.max-background-passes | 2 | passes a background pre-transcode may hold |
BACKGROUND_NICE (app.ister.server.background-nice) | 10 | nice value for background ffmpeg/ffprobe — background passes and analysis (probing, crop and intro detection, subtitle extraction) — so playback and uploads win the CPU; 0 = off |
NICE_PATH (app.ister.server.nice-path) | /usr/bin/nice | path to the nice binary used for the above |
app.ister.transcoder.hls.cache-retention-hours | 2 | a transcode cache dir is removed only after being untouched this long (and past its keep-until) |
app.ister.transcoder.hls.segment-stability-ms | 200 | a segment file must be unchanged this long before it counts as finished |
app.ister.transcoder.hls.pass-timeout-multiplier | 4 | pass timeout = media duration × this |
app.ister.transcoder.hls.pass-timeout-min-seconds | 1800 | …but never below this floor |
app.ister.transcoder.hls.pass-stall-timeout-seconds | 60 | abort a pass that writes no new segment for this long |
app.ister.transcoder.hls.upload-drain-timeout-ms | 300000 | multi-node: how long to keep uploading segments to the requesting node after a pass ends |
app.ister.server.hls.segment-timeout-ms | 60000 | how long an HTTP .ts request waits for the encoder to produce that segment |
app.ister.server.hls.master-playlist-timeout-ms | 120000 | how long a master.m3u8 request waits for playlists to be generated |
Pre-transcoding and prefetch
Two background mechanisms warm the transcode cache so playback starts instantly. Pre-transcoding works from the continue-watching list; prefetch reacts to what a play queue is doing right now. See the architecture guide, Transcoding.
| Property | Default | Notes |
|---|---|---|
app.ister.worker.pretranscode.enabled | true | checked at runtime: false stops the 15-minute pre-transcode scheduling entirely |
app.ister.server.pretranscode.keep-minutes | 30 | how long a pre-transcoded file is kept warm before it may be swept |
app.ister.server.prefetch.enabled | true | prefetch the next item in a play queue during playback |
app.ister.server.prefetch.video-threshold-seconds | 120 | start prefetching the next video once this many seconds into the current one |
app.ister.server.prefetch.track-threshold-seconds | 60 | same, for audio tracks |
app.ister.server.prefetch.track-depth | 2 | how many upcoming tracks to prefetch |
app.ister.server.prefetch.keep-hours | 24 | how long a prefetched file is kept warm |
Continue watching
| Env var | Default | Notes |
|---|---|---|
CONTINUE_WATCHING_HISTORY_DAYS | 150 | how far back the continue-watching list looks; also drives what pre-transcoding keeps warm |
CONTINUE_WATCHING_REBUILD_CRON | 0 30 3 * * * | nightly self-heal rebuild |
CONTINUE_WATCHING_REBUILD_ENABLED | true | checked at runtime: false skips both the nightly rebuild and the one-time startup backfill that fills an empty table |
Prefer these short env vars over the long property names here: the underlying properties are
bound inconsistently (…rebuild.enabled with a dot but …rebuild-cron with a dash), which the
env vars hide.
Cache cleanup and podcasts
| Setting | Default | Notes |
|---|---|---|
CACHE_CLEANUP_ENABLED / CACHE_CLEANUP_CRON | true / 0 30 4 * * * | daily zombie sweep of cache and tmp dirs |
CACHE_CLEANUP_DRY_RUN | true | it only logs until you set this to false — see Maintenance |
CACHE_CLEANUP_MIN_AGE | 24h | never deletes files younger than this |
app.ister.server.cache-cleanup.podcast-retention-days | 30 | downloaded podcast episodes expire after this, unless someone is mid-episode |
app.ister.worker.podcast.auto-download-count | 3 | newest episodes auto-downloaded per feed |
app.ister.worker.podcast.refresh-cron | 0 10 * * * * | hourly feed refresh |
app.ister.worker.podcast.refresh-min-interval-minutes | 30 | a feed is not re-fetched again within this window, even if the cron fires |
Media upload
See Uploading media for what these protect against.
| Setting | Default | Notes |
|---|---|---|
UPLOAD_ENABLED | true | switches the admin upload endpoints off entirely |
UPLOAD_CHUNK_SIZE | 16MB | one chunk request; must fit the reverse proxy's body limit, minimum 5 MB |
UPLOAD_MAX_ACTIVE_SESSIONS / UPLOAD_MAX_FILES_PER_SESSION | 2 / 100000 | uploads at once (cluster-wide), files per upload (a Steam or PS3 game is one upload of up to ~100k files) |
UPLOAD_MAX_CONCURRENT_CHUNKS | 4 | chunk requests one node handles at once |
UPLOAD_SESSION_IDLE_TIMEOUT / UPLOAD_CLEANUP_INTERVAL | 24h / PT1H | when an abandoned upload expires, and how often that is checked |
UPLOAD_MIN_FREE_SPACE | 5GB | space a local directory keeps free after an upload |
Games
The per-user save cloud of GAME libraries (see Libraries and media layout).
| Setting | Default | Notes |
|---|---|---|
GAMES_SAVE_RETENTION | 10 | save snapshots kept per user per game; older ones are deleted when a new one arrives |
GAMES_SAVE_MAX_BYTES | 512MB | largest save snapshot a client may upload |
GAMES_ACHIEVEMENT_ICON_MAX_BYTES | 512KB | largest achievement icon a client may upload |
app.ister.worker.ludusavi.manifest-url | the Ludusavi manifest on GitHub | where PC/Steam save locations come from; cached in the cache directory under ludusavi/, refreshed every refresh-hours (168); enabled: false turns the lookup off |
External metadata endpoints
Every external service the server calls is a property whose default is the real service, so you normally set none of these. They exist so a deployment can route through a proxy or mock — the chart's CI points them all at one WireMock pod:
spring.cloud.openfeign.client.config.tmdb.url, app.ister.worker.tmdb.image-base,
app.ister.worker.musicbrainz.base / .coverart-release-base / .coverart-release-group-base /
.commons-filepath-base, app.ister.worker.openlibrary.base / .covers-base /
.author-photo-base, app.ister.worker.wikidata.entity-base / .api-base,
app.ister.worker.wikipedia.summary-template, app.ister.api.podcast.itunes-base,
app.ister.worker.ludusavi.manifest-url.
Libraries and directories
app.ister.disk.libraries[n].* and app.ister.disk.directories[n].* define what gets scanned —
covered in full in Libraries and media layout. Helper nodes
are assigned other nodes' disks with app.ister.helper.disks[n].name (+ .jobs), and an owner
hands job families off with app.ister.helper.offload-jobs — see Multi-node.
Health, metrics, and other internals
The Spring Actuator (/actuator/health, /actuator/metrics, /actuator/prometheus) runs on its
own port so it stays off the public API. That port separation is the only protection — the
actuator endpoints themselves are unauthenticated — so never publish or proxy port 8081. See
Installation.
| Property | Default | Notes |
|---|---|---|
management.server.port | 8081 | port for the Actuator endpoints |
management.endpoints.web.exposure.include | health,metrics,prometheus | which Actuator endpoints are exposed |
app.ister.server.blur-hash.chunk-size | 500 | images processed per chunk during the BlurHash sweep (keeps a chunk under the RabbitMQ consumer timeout) |
app.ister.server.segment-detect.chunk-size | 4 | episodes fingerprinted per intro/outro-detection message; keep a chunk well under the RabbitMQ consumer timeout |
SEGMENT_DETECT_CONCURRENCY (app.ister.server.segment-detect.concurrency) | 1 | intro/outro-detection consumers on this node; detection for a season also waits until an upload into its directory has finished |
app.ister.server.crop-detect-backfill | true | escape hatch: false skips the one-time crop-detection re-analysis of every pre-existing file on the first scan after upgrading — see Maintenance |
app.ister.server.segment-detect-backfill | true | same escape hatch for the one-time intro/outro fingerprinting of every pre-existing episode |
Local overrides (development)
When running from source, machine-specific settings go in gitignored *-local.yaml files
next to the module's YAML file (e.g. core/src/main/resources/core-local.yaml);
./gradlew bootRun activates the local profile automatically. Container deployments should use
environment variables instead.