The sections below list every configuration key. Values in the Default column come from the code and may differ from values written in the example files. The examples are ready-to-use starting points, not a record of every built-in default.
File format and loading
Configuration comes in TOML or JSONC. The two formats carry identical keys and structure; the file extension picks the parser:
.tomlis parsed as TOML..jsonand.jsoncare parsed as JSONC://line comments,/* */block comments, and trailing commas in objects and arrays are stripped first, then the result is parsed as standard JSON. Those characters inside string literals are left alone.- Any other extension is rejected at startup, with the accepted extensions named in the error.
configs/examples/kiln.toml and configs/examples/kiln.jsonc in the repository are the same configuration written both ways. Keep local, private configuration in configs/local.toml, which is already gitignored.
The path is passed with -config and is mandatory; without it the process exits with code 2:
kiln -config /etc/kiln/kiln.tomlWhen configuration is read and validated
Configuration is read once at startup and never reloaded; edits require a restart. Loading runs in a fixed order:
Parse
Parse by extension. Syntax errors fail immediately.
Apply environment overrides
Apply the KILN_* overrides, which at this stage outrank the matching keys in the file. See Environment variables.
Fill defaults
Fill in defaults for empty or non-positive values and normalize [[channels]]: lowercase ingress, force restart_on_failure for DASH channels, and set on_demand when neither on_demand nor autostart was set.
Resolve and load the key file
Resolve packager.keys_file to an absolute path, then read and validate it in full. A failure here is a startup failure.
Validate
Run every structural check. Any failure prints the reason and exits with code 1.
Resource adaptation runs after validation. Based on detected memory and CPU ceilings it overrides server.memory_limit_mb, packager.inflight_bytes, packager.max_segment_bytes, packager.start_segments, packager.prefetch_segments, epg.max_refresh_concurrency, and epg.max_source_bytes — only downward, never upward. The startup log prints the resulting values.
What lives in the config file and what lives in the database
[[channels]], [[proxies]], and [[egress.rules]] seed the SQLite database under server.data_dir only while the corresponding table is empty. After that the admin surface owns them, and editing the config file no longer overwrites them. egress.default, egress.playlist_policy, egress.docker_proxy_host, and server.public_base_url are written with insert-if-absent semantics, so they too only apply on first run. [[upstreams]], [auth], and the remaining global keys are always taken from the file, though the username and password of an entry in [[auth.users]] can be changed from the admin surface and stored as a database override.
[server]
| Key | Type | Default | Description |
|---|---|---|---|
listen |
string | "0.0.0.0:8080" |
HTTP listen address. Environment equivalent: KILN_LISTEN. |
public_base_url |
string | "http://127.0.0.1:8080" |
Externally reachable base URL used to build playback and other outward links. A trailing slash is trimmed. Environment equivalent: KILN_PUBLIC_BASE_URL. |
data_dir |
string | "./data" |
Data directory holding SQLite, auto-generated signing keys, and the default EPG cache. Created with mode 0750 at startup. Environment equivalent: KILN_DATA_DIR. |
resource_mode |
string | "auto" |
Resource adaptation mode; one of auto, performance, constrained. Anything else fails validation. Environment equivalent: KILN_RESOURCE_MODE. |
read_timeout_sec |
int | 15 |
HTTP read timeout in seconds. Non-positive values fall back to the default. |
write_timeout_sec |
int | 0 |
HTTP write timeout in seconds; 0 means no write timeout. A write timeout cuts long-lived streaming responses, so leave it at 0 unless you specifically need one. |
idle_timeout_sec |
int | 120 |
HTTP idle timeout in seconds. Non-positive values fall back to the default. |
memory_limit_mb |
int | 0 |
Go soft memory target in MiB; 0 leaves it unset. Negative or absurdly large values fail validation. Applied only when GOMEMLIMIT is unset. |
The three resource modes
autofirst picks an internal profile from effective memory (compact below 256 MiB, balanced below 512 MiB, standard below 1 GiB, configured values kept at 1 GiB and above), then applies a CPU ceiling independently to pipeline depth and EPG concurrency.constrainedskips detection and applies the tightest compact budget outright.performanceopts out of adaptation entirely; configured values are used as written.
In every mode, adaptation only lowers configured values; it never raises a smaller value. See Resource adaptation for profile budgets, CPU ceilings, and the fixed Lite budget.
[logging]
| Key | Type | Default | Description |
|---|---|---|---|
level |
string | "info" |
Log level. Recognizes debug, info, warn, error and common aliases; unrecognized values fall back to info. Environment equivalent: KILN_LOG_LEVEL. |
format |
string | "text" |
Output format. json selects the structured handler; anything else is treated as console text. Environment equivalent: KILN_LOG_FORMAT. |
color |
string | "auto" |
Coloring policy. always forces color, never disables it, anything else behaves as auto, which colors only when the output is a terminal and NO_COLOR is unset. Only meaningful for text. Environment equivalent: KILN_LOG_COLOR. |
[auth]
| Key | Type | Default | Description |
|---|---|---|---|
token_private_key |
string | "" |
Inline Ed25519 private key PEM. Environment equivalent: KILN_TOKEN_PRIVATE_KEY. |
token_public_key |
string | "" |
Inline Ed25519 public key PEM, used only to cross-check the private key. A mismatch fails startup. Environment equivalent: KILN_TOKEN_PUBLIC_KEY. |
token_private_key_file |
string | "" |
Path to a private key PEM, read when the inline private key is empty. Environment equivalent: KILN_TOKEN_PRIVATE_KEY_FILE. |
token_public_key_file |
string | "" |
Path to a public key PEM, read when the inline public key is empty. Environment equivalent: KILN_TOKEN_PUBLIC_KEY_FILE. |
token_issuer |
string | "kiln" |
iss claim of the session JWT. |
token_audience |
string | "kiln" |
aud claim of the session JWT. |
token_ttl_hours |
int | 24 |
Session JWT lifetime in hours. Non-positive values fall back to the default. |
login_rate_per_min |
int | 20 |
Per-minute rate limit on the login endpoint. Non-positive values fall back to the default. |
users |
array | Required | User table, see [[auth.users]] below. An empty table fails validation. |
Signing keys are resolved in order: inline private key, private key file, {data_dir}/auth/ed25519.pem. In the last case, if the file does not exist Kiln generates a key pair and writes {data_dir}/auth/ed25519.pem and {data_dir}/auth/ed25519.pub.pem. At least one of token_private_key, token_private_key_file, or server.data_dir must therefore be set, or validation fails. Key material must be valid Ed25519 PEM; a wrong length or a public key that does not match the private key fails startup.
[[auth.users]]
| Key | Type | Default | Description |
|---|---|---|---|
username |
string | Required | Login name. Empty or duplicated names fail validation. |
password_hash |
string | Required | bcrypt password hash. Empty fails validation. |
role |
string | Required | Role; empty fails validation. admin is the only privileged role and reaches every admin endpoint. Every other value is treated as a restricted role. |
channel_ids |
array of string | [] |
Channel allowlist for a restricted role. Empty means all channels. admin is not constrained by it. |
Changing a username or password in the admin console writes an override row keyed by the original config username; the config file itself is never rewritten. See Authentication for the full model.
[security]
| Key | Type | Default | Description |
|---|---|---|---|
play_require_auth |
bool | true |
Whether playback endpoints require credentials. The key is optional; omitting it means true. |
allowed_hosts |
array of string | [] |
Explicit exemptions for outbound hosts that resolve to loopback or private addresses. Upstream and channel declarations do not add entries; list each private hostname or IP here. |
public_hosts |
array of string | [] |
Inbound Host allowlist. Empty means no restriction; * allows everything. /healthz and /readyz from a loopback client are always exempt. |
cors_origins |
array of string | [] |
Allowed cross-origin origins. Empty means no CORS headers are emitted at all. * is echoed back literally only when it is the single entry; otherwise the matched origin is echoed with Vary: Origin. |
max_playlist_bytes |
int64 | 8388608 |
Byte ceiling for a fetched playlist. Non-positive values fall back to the default. |
max_body_bytes |
int64 | 1048576 |
Byte ceiling for admin request bodies. Non-positive values fall back to the default. A few bulk endpoints multiply it. |
Outbound requests also pass a fixed guard: link-local, unspecified, multicast, and cloud metadata addresses are always refused, while loopback or private addresses are reachable only when explicitly listed in security.allowed_hosts.
[packager]
| Key | Type | Default | Description |
|---|---|---|---|
engine |
string | "auto" |
Default packaging engine; one of auto, native, ffmpeg. Anything else fails validation. auto prefers native and falls back to ffmpeg for unsupported sources, native never falls back, ffmpeg always remuxes. When left empty, the value comes from KILN_DEFAULT_PACKAGER_ENGINE. |
keys_file |
string | "" |
Path to the global kid:key catalog. Relative paths resolve against the config file’s directory. |
playlist_size |
int | 8 |
Segments retained in the output playlist. Non-positive values fall back to the default. |
ll_hls |
bool | false |
Enables CMAF parts, delta playlists, and blocking reload. The example config turns it on, but the underlying default is off. |
part_target_ms |
int | 500 |
LL-HLS part target in milliseconds. Must be between 100 and 5000, otherwise validation fails. |
start_segments |
int | 3 |
Segments prepared during cold start. Non-positive values fall back to the default. Resource adaptation may lower it. |
prefetch_segments |
int | 3 |
Segments prefetched in steady state. Non-positive values fall back to the default. Resource adaptation may lower it. |
max_segment_bytes |
int64 | 33554432 |
Byte ceiling for a single segment; anything larger is treated as a fault. Non-positive values fall back to the default. Resource adaptation may lower it. |
grace_sec |
int | 30 |
Grace period, in seconds, during which a segment remains fetchable after leaving the playlist. Non-positive values fall back to the default. |
primary_track_hold_sec |
int | 12 |
How far, in media seconds, audio may run ahead of video. Non-positive values fall back to the default. It bounds playlist window advancement, not A/V sync. |
stall_timeout_sec |
int | 180 |
Fails a publication whose manifest keeps updating while nothing reaches the playlist. -1 disables it; 0 falls back to the default. An unreachable upstream is a different case and keeps retrying. |
inflight_bytes |
int64 | 100663296 |
Segment memory budget shared across all channels, in bytes. Non-positive values fall back to the default. Resource adaptation may lower it. |
The inflight_bytes trade-off
This value is what decides peak memory. A single 4K segment is tens of megabytes, so the budget is counted in bytes rather than segments — otherwise memory would become a function of the source bitrate. What it buys is cold-start latency and nothing else: in steady state each track pulls one segment per refresh and never approaches the budget. Lowering it reduces resident memory at the cost of how quickly a 4K channel produces its first playlist.
How keys_file is loaded and validated
keys_file is read and validated in full once at startup. A single bad line fails the boot:
- One
kid:keypair per line. Blank lines and lines starting with#are ignored. - A missing colon, an empty
kid, or an emptykeyis reported with the line number. kidmust be 32 hexadecimal characters and may be written in dashed UUID form; dashes are removed before comparison.keymust be 32 hexadecimal characters and must not contain dashes.- A repeated
kidis ignored when the key matches and rejected when it does not. - The file must yield at least one pair.
Both kid and key are normalized by removing dashes and lowercasing. Keys never appear in the admin API, and edits require a restart. Any DASH channel that is not disabled requires a non-empty global catalog, or validation fails. See Media engine for engine selection and media handling.
[ffmpeg]
| Key | Type | Default | Description |
|---|---|---|---|
mode |
string | "native" |
Execution mode. native runs a local binary, docker lets Kiln launch a given image. Anything else fails validation. |
binary |
string | "ffmpeg" |
Executable name or path used in native mode. |
docker_image |
string | "kiln:local" |
Image used in docker mode; ignored otherwise. |
hls_time |
int | 2 |
Target segment duration in seconds for remuxed output. Non-positive values fall back to the default. |
hls_list_size |
int | 8 |
Segments retained in remuxed output playlists. When unset or non-positive, the default is 4 if low_latency is true and 8 otherwise. |
log_level |
string | "error" |
Log level passed to ffmpeg. |
prefer_height |
int | 0 |
Global preferred video height; 0 means no preference. A positive per-channel prefer_height overrides it. |
low_latency |
bool | false |
Picks the default for hls_list_size: 4 when true, 8 otherwise. An explicit hls_list_size always wins. |
max_starts |
int | 0 |
Ceiling on concurrent ffmpeg launches; non-positive values behave as 1. It covers the launch only, not the readiness wait, so a slow source does not block other channels’ cold start. |
[observe]
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Whether /metrics is served and OTLP export runs. The key is optional; omitting it means true. An explicit false makes /metrics return 404 and skips exporter setup entirely. |
otlp_endpoint |
string | "" |
OTLP/HTTP trace export endpoint; empty disables export. When set it must be an absolute http or https URL with a host, otherwise validation fails. |
otlp_insecure |
bool | false |
Whether insecure transport is allowed for export. |
trace_sample_ratio |
float | 1 |
Sampling ratio, constrained to 0 through 1. Non-positive values are rewritten to 1 while defaults are filled in. |
service_name |
string | "kiln" |
service.name reported to OTLP. |
[debug.pprof]
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Whether the dedicated pprof server starts. When off, no listener is created at all. |
listen |
string | "127.0.0.1:6060" |
pprof listen address. When enabled it must parse as host and port, and the host must be a loopback IP, otherwise validation fails. |
pprof uses its own listener and mux and is never mounted on the public server. Turn it on only while collecting CPU, heap, block, or mutex profiles. See Troubleshooting for diagnostic workflows.
[epg]
| Key | Type | Default | Description |
|---|---|---|---|
cache |
bool | true |
Whether the guide cache is used. Omitting the key means enabled. |
cache_dir |
string | "{data_dir}/epg" |
Cache directory. memory or :memory: switches to an in-memory store; any other value is treated as a directory path. |
refresh_interval_min |
int | 360 |
Refresh interval in minutes. Non-positive values fall back to the default. |
max_refresh_concurrency |
int | 0 |
Ceiling on sources refreshed concurrently; 0 means unlimited (every source in one pass). Negative values fail validation. Resource adaptation may lower it, including tightening 0 into a concrete number. |
max_source_bytes |
int64 | 67108864 |
Byte ceiling per source; larger payloads are rejected. Non-positive values fall back to the default. Resource adaptation may lower it. |
default_timezone |
string | "UTC" |
Timezone used when a source declares none. Must be a loadable timezone name, otherwise validation fails. |
serve_timezone |
string | "keep" |
Output timezone policy. Only keep is accepted, meaning timestamps are served as-is. |
sources |
array | [] |
Guide sources, see [[epg.sources]] below. |
[[epg.sources]]
| Key | Type | Default | Description |
|---|---|---|---|
id |
string | Required | Source identifier. Empty or duplicated values fail validation. |
name |
string | "" |
Display name. |
url |
string | "" |
XMLTV address. When set it must be an absolute http or https URL with a host. |
timezone |
string | "" |
Timezone for this source; must be loadable when set. Empty falls back to epg.default_timezone. |
proxy |
string | "direct" |
Outbound route: direct, auto, or a [[proxies]] id. Unknown values fail validation. Empty is filled in as direct. |
enabled |
bool | false |
Whether the source participates in refreshes. |
See EPG for channel matching and built-in source behavior.
[[proxies]]
| Key | Type | Default | Description |
|---|---|---|---|
id |
string | Required | Route identifier, referenced by egress.default, [[egress.rules]], and [[epg.sources]]. direct is reserved and means no proxy. |
name |
string | "" |
Display name. |
url |
string | Required | Proxy address. Schemes are limited to http, https, socks5, socks5h — for example http://127.0.0.1:7890 or socks5h://127.0.0.1:7891. |
disabled |
bool | false |
Whether the route is inactive. A disabled route takes no traffic, and decisions pointing at it fall back to direct. |
[egress]
| Key | Type | Default | Description |
|---|---|---|---|
default |
string | "direct" |
Route used when no rule matches. Must be direct or a defined [[proxies]] id, otherwise validation fails. |
playlist_policy |
string | "rewrite" |
Playlist URL rewriting policy; one of rewrite, passthrough, auto. Anything else fails validation. rewrite always rewrites to a Kiln-relative URL, passthrough always keeps the original, auto rewrites only when a proxy was actually used. |
docker_proxy_host |
string | "host.docker.internal" |
Applies only when ffmpeg.mode = "docker"; the hostname the spawned ffmpeg container uses to reach the Kiln process. It does not affect [[proxies]].url — Kiln never rewrites a route address. |
rules |
array | [] |
Routing rules, see [[egress.rules]] below. |
[[egress.rules]]
| Key | Type | Default | Description |
|---|---|---|---|
id |
string | "" |
Rule identifier. When empty, seeding generates rule-1, rule-2, and so on. |
priority |
int | 0 |
Match order; lower runs first and the first match wins. |
kind |
string | "host_suffix" |
Match type; one of host_suffix, host_exact, host_regex, channel_id, url_regex. Empty behaves as host_suffix. |
pattern |
string | "" |
Match input. Except for channel_id, a rule with an empty pattern is skipped. Regex kinds compile with Go’s regexp syntax; a compile failure counts as no match. |
proxy |
string | Required | Route used on a match. Must be direct or a defined [[proxies]] id; empty or unknown fails validation. |
disabled |
bool | false |
Whether the rule is inactive. |
See Outbound proxying for route selection, rewriting policies, and container scenarios.
[[upstreams]]
| Key | Type | Default | Description |
|---|---|---|---|
id |
string | Required | Upstream identifier referenced by a channel’s upstream. Empty fails validation. |
base_url |
string | Required | Upstream base address. Must parse as an absolute URL; empty or malformed fails validation. A host that resolves to a loopback or private address must also be listed explicitly in security.allowed_hosts. |
headers |
table | {} |
Fixed headers added to requests toward this upstream. |
[[channels]]
| Key | Type | Default | Description |
|---|---|---|---|
id |
string | Required | Channel identifier. Empty, ., .., path separators, control characters, and duplicates all fail validation. |
title |
string | "" |
Display name. |
group |
string | "" |
Group label used in playlists and the admin UI. |
logo_url |
string | "" |
Logo address. |
epg_id |
string | "" |
Identifier matched exactly against the channel id in XMLTV. |
epg_name |
string | "" |
Name used when matching XMLTV channels by name; falls back to title when empty. |
epg_source |
string | "" |
Restricts matching to one [[epg.sources]] id. |
source_url |
string | "" |
Absolute source address. When set it must be http or https, carry a host, and contain no fragment; upstream and path are then unnecessary. |
upstream |
string | Conditional | The [[upstreams]] id to use. Required and must exist when source_url is absent, otherwise validation fails. |
path |
string | Conditional | Path appended to the upstream base_url. Required when source_url is absent. |
ingress |
string | "hls" |
Source type; hls or dash, lowercased before validation. Anything else fails validation. |
disabled |
bool | false |
Whether the channel is inactive. A disabled DASH channel no longer requires the global key catalog. |
on_demand |
bool | true |
Whether the channel pulls on demand. When both on_demand and autostart are false, defaults set on_demand to true. |
autostart |
bool | false |
Whether the channel starts pulling at boot. |
idle_timeout_sec |
int | 90 |
Seconds a session survives with no viewers. Non-positive values fall back to the default. |
max_viewers |
int | 0 |
Concurrent viewer ceiling; 0 means unlimited. |
user_agent |
string | "" |
User-Agent used when fetching this channel. |
headers |
table | {} |
Fixed headers added when fetching this channel. |
restart_on_failure |
bool | false |
Whether the session restarts after a failure. Channels with ingress = "dash" are forced to true. |
prefer_height |
int | 0 |
Preferred video height; 0 inherits ffmpeg.prefer_height. |
preferred_audio_languages |
array of string | [] |
Audio language preference, applied in order. A non-empty selection.audio.preferred_languages takes precedence. |
packager |
string | "" |
Packaging engine for this channel; empty inherits packager.engine. When set it must be auto, native, or ffmpeg. |
selection |
table | {} |
Fine-grained track selection, see below. |
[channels.selection]
Selection is split into video, audio, and subtitles. Each group has a mode and a track selector.
| Key | Type | Default | Description |
|---|---|---|---|
video.mode |
string | "" |
One of auto, cap, exact; empty behaves as auto. exact requires at least one field in video.track, otherwise validation fails. |
video.max_height |
int | 0 |
Video height ceiling. A positive value overrides prefer_height. |
video.max_frame_rate |
string | "" |
Frame rate ceiling. Currently only length- and control-character-validated; it does not yet influence selection. |
video.track |
table | {} |
Video track selector, fields below. |
audio.mode |
string | "" |
One of auto, prefer, only; empty behaves as auto. only requires at least one field in audio.track, otherwise validation fails. |
audio.preferred_languages |
array of string | [] |
Audio language preference. Empty falls back to the channel-level preferred_audio_languages. |
audio.track |
table | {} |
Audio track selector. |
subtitles.mode |
string | "" |
One of auto, off, prefer, only; empty behaves as auto. Both prefer and only require at least one field in subtitles.track. |
subtitles.track |
table | {} |
Subtitle track selector. |
All three selectors share the same fields, and any non-empty field counts as specified:
| Key | Type | Description |
|---|---|---|
key |
string | Unique track key. |
adaptation_set_id |
string | DASH AdaptationSet id. |
representation_id |
string | DASH Representation id. |
language |
string | Language code. |
role |
string | Track role. |
codec |
string | Codec identifier. |
height |
int | Video height; only a positive value counts as specified. |
frame_rate |
string | Frame rate. |
Every string field is limited to 512 characters and must contain no control characters, otherwise validation fails. With the ffmpeg engine, a subtitles.mode of prefer or only fails validation outright; only auto and off are available. See Fine-grained track selection for practical guidance.