Config keys reference
Every configuration key recognized by the Go daemon, with its default. Keys
resolve through the scoped cascade (deployment → stack → environment → tenant
→ global). Set any key via shc config set <key> <value> (add --scope and
context flags to target a tier) or, for a subset, an environment variable
(see environment-variables.md).
Mirrors core/config/default.yaml.
Every key below traces to a real read site in the Go daemon; the Python-era
sections (api, limits, sse, websocket, collector, logs,
database, deploy, mounts, the old flat timeout, and keycloak.url /
keycloak.realm) were removed — they have zero readers in the Go port.
Sections
Section titled “Sections”- Top-level scalars
keycloakrate_limittimingcronstimeoutsmaintenancebackupconcurrencyacmednsvaultotelnotificationstoragehealthclusterrepos/clusters
Top-level scalars
Section titled “Top-level scalars”| Key | Type | Default | Notes |
|---|---|---|---|
dev | bool | false | Development mode. |
launcher | bool | true | Inject the SHC launcher button into gated apps’ HTML. Resolved through the scope cascade; global kill-switch is shc config set launcher false. |
keycloak
Section titled “keycloak”Managed-only. The keycloak URL is derived from SHC_HOST
(https://<SHC_HOST>/keycloak) — there is no url, realm, internal_url,
or base_path key.
| Key | Type | Default |
|---|---|---|
keycloak.client_id | str | shc-api |
keycloak.client_secret | null | str | null (use ref+vault://…) |
keycloak.tls_verify | bool | true (set false only for self-signed dev stacks) |
keycloak.swagger_client_id | str | shc-swagger |
rate_limit
Section titled “rate_limit”Hot-reloadable; off by default.
| Key | Type | Default |
|---|---|---|
rate_limit.requests | int | 100 |
rate_limit.window | int (seconds) | 60 |
timing
Section titled “timing”Polling intervals. Reader: core/timeouts / the loops that consume them.
| Key | Type | Default |
|---|---|---|
timing.slow_poll | float (seconds) | 1.0 |
timing.tick_interval | str | every 2 seconds |
Open map — override any registered cron’s schedule by name (crons.<cron_name>).
Every cron ships a default in code; the notable ones:
| Key | Default (in code) |
|---|---|
crons.image_prune_cron | every 6 hours |
crons.disk_auto_prune_cron | every 1 hour |
crons.cleanup_cron | every 1 hour |
crons.db_analyze_cron | every 6 hours |
crons.db_vacuum_cron | every 6 hours |
timeouts
Section titled “timeouts”Role-named operational deadlines, in SECONDS. A missing / non-positive value
falls back to the built-in default (the value shown). Reader:
core/timeouts (timeouts.Get(role)).
| Key | Default | Key | Default |
|---|---|---|---|
timeouts.keycloak | 10 | timeouts.compose_converge | 900 |
timeouts.db_connect | 10 | timeouts.container_stop | 10 |
timeouts.rqlite_ready | 60 | timeouts.exec_idle | 1800 |
timeouts.rqlite_shutdown | 10 | timeouts.exec_kill_wait | 5 |
timeouts.drain | 300 | timeouts.log_idle | 300 |
timeouts.mesh_ping | 10 | timeouts.enroll | 60 |
timeouts.quiesce | 30 | timeouts.git_clone | 600 |
timeouts.backup | 1800 | timeouts.git_fetch | 300 |
maintenance
Section titled “maintenance”Reader: services_seams_maint.go. Cron schedules live in crons.*, not here.
| Key | Type | Default |
|---|---|---|
maintenance.events_log_retention_days | int | 14 |
maintenance.events_log_max_rows | int | 0 (0 = off) |
maintenance.disk_auto_prune_threshold | int (percent) | 80 |
maintenance.stale_jobs_retention_hours | int | 24 |
backup
Section titled “backup”| Key | Type | Default |
|---|---|---|
backup.repo | null | str | null (defaults to {state_home}/backups) |
backup.password | null | str | null (use ref+vault://…) |
backup.aws_access_key_id | null | str | null |
backup.aws_secret_access_key | null | str | null |
backup.azure_account_name | null | str | null |
backup.azure_account_key | null | str | null |
backup.b2_account_id | null | str | null |
backup.b2_account_key | null | str | null |
backup.google_application_credentials | null | str | null |
backup.retention.daily | null | int | null |
backup.retention.weekly | null | int | null |
backup.retention.monthly | null | int | null |
backup.retention.yearly | null | int | null |
concurrency
Section titled “concurrency”| Key | Type | Default | Notes |
|---|---|---|---|
concurrency.queue_max_depth | int | 1000 | Queue backpressure ceiling (hot-reloadable). |
| Key | Type | Default |
|---|---|---|
acme.email | null | str | null |
Provider definitions are config. A per-host DNS target is deployment
STATE — set with shc install/update --host … (e.g.
--host app.example.com=lb.example.com), not shc config set. Three scoped
knobs ARE config: dns.public_address — the NON-INTERNET cluster entrypoint
(an ip or hostname) that --host records target on LAN topologies (target
ladder rung 3, above the traefik-VIP and node rungs; internet exposure never
reads it — internet endpoints are inferred from the system traefik’s web-conn
VIP or the placed node’s public_address) — plus the per-record dns.ttl /
dns.proxied knobs.
| Key | Type | Default |
|---|---|---|
dns.provider | null | str | null (null disables managed DNS) |
dns.providers.<name>.* | map | null — open prefix; per-provider credentials/config |
dns.public_address | str | unset — the non-internet entrypoint; scoped |
dns.ttl | int | 300 (1..86400); scoped |
dns.proxied | bool | false (Cloudflare orange-cloud); scoped |
dns.providers is an open prefix (like acme.providers / vault.providers /
otel.providers): <name> selects the impl and each impl reads its own field
set (providerBuilders, modules/dns/factory.go) — cloudflare,
route53, hetzner, powerdns, powerdns-local, rfc2136, digitalocean,
gcp, azure, linode, vultr, scaleway, exoscale, ovh, unbound,
namecheap (designate is connection-backed only). Legacy fallback path:
dns.<name>.<field>.
| Key | Type | Default |
|---|---|---|
vault.provider | null | str | null (null disables ref+vault resolution) |
vault.providers | null | map | null |
OTLP-HTTP export. otel.provider selects a otel.providers.<name> blob;
when unset a managed default (system openobserve behind SHC_HOST) engages.
| Key | Type | Default |
|---|---|---|
otel.provider | null | str | null |
otel.providers | null | map | null — per-provider {endpoint, insecure, url_path_base, headers} |
otel.filter.quiet | null | str | list | null — regex/literal pattern(s); a container-log line matching one is dropped at source for the row’s scope (the per-app “quiet a chatty container” knob — container stdout has no parsed severity, so this is a body match, not a severity floor); scoped |
otel.filter.rules | null | str | list | null — raw OTTL log_record condition(s), accepted verbatim (the T2 escape hatch); scoped |
otel.filter.* rows are scoped like every other config row — the scope of
the row is what it filters: a stack-scope quiet rule affects that stack
only. The compiled collector config conjoins every rule with a guard on the
row’s scope, so a rule can only narrow its own scope, never widen it; a
raw rule whose expression pins a scope attribute to a different value than
its own row is warned about (and still applied scope-guarded), not silently
applied. Filters drop; the always-on scrub redacts — no filter rule
is ever the security mechanism, and no rule set removes the scrub stage.
otel.provider at a NON-global scope is a routing row (telemetry
slice 5): the row’s scope is what it routes, and routing is fan-out
(owner ruling 2026-07-31) — a tenant-scope row sends that scope’s container
logs and -i otel=OTEL app traces/metrics to the named
otel.providers.<name> destination in addition to the global/default
destination, never instead of it. The provider blob resolves through the
scoped cascade first (a tenant-scoped otel.providers.<name>.* override —
per-tenant credentials — beats the global definition), then the yaml tree.
Node metrics (host + docker stats) never route to a scoped destination, and
every routed branch carries the same always-on scrub. The airgap hard-off
(license.offline) beats routing rows exactly as it beats the default
destination. A global change re-converges the per-node collector
immediately; a scoped routing row lands within one reconcile tick (≤5m).
notification
Section titled “notification”| Key | Type | Default |
|---|---|---|
notification.providers | null | map | null — named transports (type: email|ntfy|slack|discord|webhook) |
notifications | null | list | null — rule list referencing a provider by name |
storage
Section titled “storage”| Key | Type | Default |
|---|---|---|
storage.volumes.auto_failover | bool | false — OFF by default; auto-rebind an RWO volume off a dead node only after it is provably fenced |
health
Section titled “health”| Key | Type | Default |
|---|---|---|
health.reconcile_enabled | null | bool | null (null = follow cluster-mode default, leader-only) |
health.check_infrastructure | bool | true |
health.check_docker_networks | bool | true |
cluster
Section titled “cluster”There is no config knob for single-node vs cluster mode. Every binary can
cluster (the raft engine is the external rqlited process); the decision is
the engine marker {state}/engine.json, written by the promote/join flip
(core/rqlited). No marker = single-node.
Precedence
Section titled “Precedence”DB-backed scoped overrides follow the one SHC scope ladder — most specific
wins (core/scope):
- Deployment — tenant + stack + environment (
-t X -s Y -e Z,--scope deployment) - Stack — tenant + stack (
-t X -s Y,--scope stack) - Environment — tenant + environment (
-t X -e Z,--scope environment) - Tenant — tenant (
-t X,--scope tenant) - Global — base (
--scope global)
Stack outranks Environment. The same ladder orders vault secret resolution.
Across config sources, values resolve in this order (later wins):
default.yaml(bundled)/etc/shc/config.yaml+/etc/shc/config.d/*.yaml(or~/.config/shc/config.yamlfor non-root)- DB-backed scoped overrides (the ladder above)
- Environment variables (
SHC_*)
Source: core/config/default.yaml.