Skip to content
SHC Docs

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.

KeyTypeDefaultNotes
devboolfalseDevelopment mode.
launcherbooltrueInject the SHC launcher button into gated apps’ HTML. Resolved through the scope cascade; global kill-switch is shc config set launcher false.

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.

KeyTypeDefault
keycloak.client_idstrshc-api
keycloak.client_secretnull | strnull (use ref+vault://…)
keycloak.tls_verifybooltrue (set false only for self-signed dev stacks)
keycloak.swagger_client_idstrshc-swagger

Hot-reloadable; off by default.

KeyTypeDefault
rate_limit.requestsint100
rate_limit.windowint (seconds)60

Polling intervals. Reader: core/timeouts / the loops that consume them.

KeyTypeDefault
timing.slow_pollfloat (seconds)1.0
timing.tick_intervalstrevery 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:

KeyDefault (in code)
crons.image_prune_cronevery 6 hours
crons.disk_auto_prune_cronevery 1 hour
crons.cleanup_cronevery 1 hour
crons.db_analyze_cronevery 6 hours
crons.db_vacuum_cronevery 6 hours

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)).

KeyDefaultKeyDefault
timeouts.keycloak10timeouts.compose_converge900
timeouts.db_connect10timeouts.container_stop10
timeouts.rqlite_ready60timeouts.exec_idle1800
timeouts.rqlite_shutdown10timeouts.exec_kill_wait5
timeouts.drain300timeouts.log_idle300
timeouts.mesh_ping10timeouts.enroll60
timeouts.quiesce30timeouts.git_clone600
timeouts.backup1800timeouts.git_fetch300

Reader: services_seams_maint.go. Cron schedules live in crons.*, not here.

KeyTypeDefault
maintenance.events_log_retention_daysint14
maintenance.events_log_max_rowsint0 (0 = off)
maintenance.disk_auto_prune_thresholdint (percent)80
maintenance.stale_jobs_retention_hoursint24
KeyTypeDefault
backup.reponull | strnull (defaults to {state_home}/backups)
backup.passwordnull | strnull (use ref+vault://…)
backup.aws_access_key_idnull | strnull
backup.aws_secret_access_keynull | strnull
backup.azure_account_namenull | strnull
backup.azure_account_keynull | strnull
backup.b2_account_idnull | strnull
backup.b2_account_keynull | strnull
backup.google_application_credentialsnull | strnull
backup.retention.dailynull | intnull
backup.retention.weeklynull | intnull
backup.retention.monthlynull | intnull
backup.retention.yearlynull | intnull
KeyTypeDefaultNotes
concurrency.queue_max_depthint1000Queue backpressure ceiling (hot-reloadable).
KeyTypeDefault
acme.emailnull | strnull

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.

KeyTypeDefault
dns.providernull | strnull (null disables managed DNS)
dns.providers.<name>.*mapnull — open prefix; per-provider credentials/config
dns.public_addressstrunset — the non-internet entrypoint; scoped
dns.ttlint300 (1..86400); scoped
dns.proxiedboolfalse (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>.

KeyTypeDefault
vault.providernull | strnull (null disables ref+vault resolution)
vault.providersnull | mapnull

OTLP-HTTP export. otel.provider selects a otel.providers.<name> blob; when unset a managed default (system openobserve behind SHC_HOST) engages.

KeyTypeDefault
otel.providernull | strnull
otel.providersnull | mapnull — per-provider {endpoint, insecure, url_path_base, headers}
otel.filter.quietnull | str | listnull — 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.rulesnull | str | listnull — 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).

KeyTypeDefault
notification.providersnull | mapnull — named transports (type: email|ntfy|slack|discord|webhook)
notificationsnull | listnull — rule list referencing a provider by name
KeyTypeDefault
storage.volumes.auto_failoverboolfalse — OFF by default; auto-rebind an RWO volume off a dead node only after it is provably fenced
KeyTypeDefault
health.reconcile_enablednull | boolnull (null = follow cluster-mode default, leader-only)
health.check_infrastructurebooltrue
health.check_docker_networksbooltrue

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.

DB-backed scoped overrides follow the one SHC scope ladder — most specific wins (core/scope):

  1. Deployment — tenant + stack + environment (-t X -s Y -e Z, --scope deployment)
  2. Stack — tenant + stack (-t X -s Y, --scope stack)
  3. Environment — tenant + environment (-t X -e Z, --scope environment)
  4. Tenant — tenant (-t X, --scope tenant)
  5. Global — base (--scope global)

Stack outranks Environment. The same ladder orders vault secret resolution.

Across config sources, values resolve in this order (later wins):

  1. default.yaml (bundled)
  2. /etc/shc/config.yaml + /etc/shc/config.d/*.yaml (or ~/.config/shc/config.yaml for non-root)
  3. DB-backed scoped overrides (the ladder above)
  4. Environment variables (SHC_*)

Source: core/config/default.yaml.