config.toml reference

Ring reads ~/.config/kemeter/ring/config.toml (or $RING_CONFIG_DIR/config.toml) at startup. To load a different file (e.g. to keep config_dev.toml and config_prod.toml side by side), pass --config <path> or set RING_CONFIG_FILE (see CLI reference). It is split in two:

  • [contexts.<name>] is the client config: how a CLI reaches a server (host, API, auth). There can be several (e.g. local, staging, prod), like kubectl contexts.
  • [server] is the daemon config: what the Ring server does (which runtimes it enables, scheduler interval, dashboard). One shared table, outside [contexts.*].

A context describes one client→server connection; it has no business deciding which runtimes that server enables, which is why daemon settings live under their own top-level [server] table.

ring init writes this file with the runtimes you select enabled. Ring falls back to a sensible default context if no file exists.

Top-level shape

[server]                                  # daemon config (shared)
[server.scheduler]                        # optional
[server.dashboard]                        # optional
[server.exec]                             # opt-in: enabled = true
[server.telemetry.traces]                 # opt-in: enabled = true
[server.telemetry.metrics]                # opt-in: enabled = true
[server.telemetry.logs]                   # opt-in: enabled = true
[server.runtime.docker]                   # opt-in: enabled = true
[server.runtime.cloud_hypervisor]         # opt-in: enabled = true

[contexts.<name>]                         # client config (one or more)
current = true
host    = "..."
api     = { ... }

You can declare multiple [contexts.<name>] tables in the same file. The one with current = true is the default; switch with the --context flag on most CLI commands. The single [server] table applies whichever context is active.

Runtimes are opt-in

No container runtime is enabled by default. Ring registers a runtime only when you turn it on with enabled = true under [server.runtime.<runtime>]. A runtime you don't enable is never touched, even if its socket or binary is present. This is what lets the same Ring build run Docker-only, Cloud-Hypervisor-only, or any mix.

Two rules follow:

  • At least one runtime must be enabled. With none, Ring refuses to start (it could not deploy anything).
  • An enabled-but-unreachable runtime is a hard error. Enable Docker but the daemon doesn't answer, or enable Cloud Hypervisor but its binary can't be found, and Ring fails fast at startup with a clear message, rather than starting and returning a 500 on the first deployment.

Fields

[contexts.<name>]

FieldTypeRequiredDefaultPurpose
currentboolyesnoneMark this context as the default. Exactly one context should be true per file
hoststringyesnoneThe IP or hostname the server binds to. Set "127.0.0.1" for loopback-only; "0.0.0.0" to listen on every interface. The CLI uses the same value to reach the server
apiinline tableyesnoneSee api
userinline tableyesnoneSee user

Daemon settings (runtimes, scheduler, dashboard) are not under the context; see [server] below.

[contexts.<name>.api]

FieldTypeRequiredDefaultPurpose
schemestringyesnone"http" or "https". Used to build the API URL the CLI talks to. Ring itself does not terminate TLS, so set "https" only when fronted by a reverse proxy
portintyesnoneTCP port. Default in the auto-fallback context is 3030; explicit configs must set it
cors_originsarray of stringno[]List of Origin values allowed by the API's CORS layer. Leave empty to disallow browser cross-origin calls

No password salt to configure. Earlier versions required a [contexts.<name>.user] table with a global salt. Ring now generates a unique random salt for every password hash, so there is nothing to set or keep secret. A leftover user.salt line in an existing config is ignored.

[server]

The daemon's own configuration, shared by every context in the file. All subsections optional.

[server.scheduler]

FieldTypeRequiredDefaultPurpose
intervalint (seconds)no10Reconciliation tick interval. Overridden by RING_SCHEDULER_INTERVAL if set

[server.dashboard]

FieldTypeRequiredDefaultPurpose
enabledboolnofalseSpawn the embedded dashboard. Also flippable via --dashboard / RING_DASHBOARD
listen_addressstringno"127.0.0.1:3031"host:port the dashboard binds to. Override with RING_DASHBOARD_LISTEN

[server.exec]

Interactive ring deployment exec sessions. Off by default: handing out a shell inside a running workload is a capability you switch on deliberately, not one that appears on upgrade. While disabled the endpoint answers 501.

FieldTypeRequiredDefaultPurpose
enabledboolnofalseAllow exec sessions at all
max_duration_secondsintno14400 (4 h)Hard ceiling on one session; it is disconnected when reached
idle_timeout_secondsintno900 (15 min)Disconnect after this long with no traffic either way
max_concurrent_sessionsintno16Daemon-wide limit; further requests get 503. 0 disables exec
[server.exec]
enabled = true
idle_timeout_seconds = 300
max_concurrent_sessions = 4

The two timeouts exist because a container runtime gives no way to kill an exec once it has started: there is no delete call, and dropping the connection leaves the process running inside the container. Ring cannot close that gap, so it bounds its own participation instead: past these limits it stops relaying and frees the slot. A command that ignores its closed stdin keeps running until the container stops.

The idle timeout is the one that matters in practice: a client that vanished without closing (a laptop lid, a dropped VPN) stops producing traffic long before anything else notices, and that is what would otherwise hold a slot open indefinitely.

Only a session's own instance is reachable. An exec request naming a container Ring does not manage is refused the same way a non-existent one is.

Every session that opens is written to the namespace's audit trail (GET /namespaces/{id}/audit, action: "exec"), alongside deployment and secret writes. A refused exec records nothing, so the trail reflects sessions that actually happened.

What is recorded is that a session opened, by whom, on which deployment and when. The command is deliberately not stored (arguments carry credentials often enough), and neither is anything typed during the session. If you need to know what was done inside a shell, this is not that, and no configuration here provides it.

[server.telemetry.traces]

Opt-in OpenTelemetry span export over OTLP/gRPC. Off by default: with enabled = false no exporter is built and the server runs exactly as before. Only ring server start exports traces; the CLI commands stay console-only.

FieldTypeRequiredDefaultPurpose
enabledboolnofalseExport spans to an OTLP/gRPC collector
endpointstringno"http://127.0.0.1:4317"Collector endpoint. Override with OTEL_EXPORTER_OTLP_ENDPOINT (or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)
service_namestringno"ring-server"service.name resource attribute. Override with OTEL_SERVICE_NAME
samplerstringno"parent_based_always_on"always_on, always_off, parent_based_always_on, parent_based_always_off, or ratio:<0..1> (e.g. ratio:0.1 for 10% of roots)

Standard OTEL_* environment variables take precedence over the TOML values, so a deployment can point Ring at its collector without editing the file. If the exporter fails to initialise (unreachable endpoint at startup, etc.) Ring logs the error and continues without traces rather than failing.

Ring emits one span per HTTP request (named {method} {route} with the stable HTTP-server semantic-convention attributes) and one scheduler.cycle span per reconciliation cycle that has work to do — idle scheduler ticks produce no spans.

Each telemetry signal is independent: you can enable traces, metrics, or logs on their own or in any combination. All three are off by default.

[server.telemetry.metrics]

Opt-in OpenTelemetry metric export (push) over OTLP/gRPC. Off by default. When enabled, Ring periodically pushes the same per-deployment resource series the Prometheus /metrics endpoint serves, read from the background stats cache (so enabling it adds no extra runtime round-trip). Only ring server start exports metrics.

FieldTypeRequiredDefaultPurpose
enabledboolnofalsePush metrics to an OTLP/gRPC collector
endpointstringno"http://127.0.0.1:4317"Collector endpoint. Override with OTEL_EXPORTER_OTLP_ENDPOINT (or OTEL_EXPORTER_OTLP_METRICS_ENDPOINT)
service_namestringno"ring-server"service.name resource attribute. Override with OTEL_SERVICE_NAME
interval_secondsintno15Push interval. Floored at 1s

The exported gauges, labelled {deployment, namespace, runtime}, are: ring.deployment.cpu.percent, ring.deployment.memory.used_bytes, ring.deployment.memory.limit_bytes, ring.deployment.network.rx_bytes, ring.deployment.network.tx_bytes, ring.deployment.disk.read_bytes, ring.deployment.disk.write_bytes, ring.deployment.pids, ring.deployment.instances, and ring.deployment.restarts. Values are at most one stats-cache refresh (the scheduler interval) stale. A failed exporter is logged and the server continues without OTLP metrics.

[server.telemetry.logs]

Opt-in OpenTelemetry log export (push) over OTLP/gRPC. Off by default. When enabled, Ring's log events are bridged to an OTLP collector in addition to the console — console logging is never turned off. Only ring server start exports logs.

FieldTypeRequiredDefaultPurpose
enabledboolnofalseExport log records to an OTLP/gRPC collector
endpointstringno"http://127.0.0.1:4317"Collector endpoint. Override with OTEL_EXPORTER_OTLP_ENDPOINT (or OTEL_EXPORTER_OTLP_LOGS_ENDPOINT)
service_namestringno"ring-server"service.name resource attribute. Override with OTEL_SERVICE_NAME

A failed exporter is logged and the server continues console-only rather than failing.

[server.runtime.docker]

FieldTypeRequiredDefaultPurpose
enabledboolnofalseRegister the Docker runtime. Must be true for Ring to use Docker. When true and the daemon is unreachable at startup, Ring fails fast
hoststringno"unix:///var/run/docker.sock"Docker daemon URL. Use tcp://host:2375 for a remote daemon, tcp://host:2376 for TLS
use_host_registry_authboolnofalseAuthorize deployments to resolve registry credentials from the host Docker config (see host registry auth). A deployment must also set config.use_host_auth: true to activate it
host_registry_configstringnounsetExplicit path to the host registry config (config.json schema). When unset, standard Docker resolution applies ($DOCKER_CONFIG, then ~/.docker/config.json)

[server.runtime.podman]

Podman speaks the Docker-compatible API (podman system service), so Ring drives it with the same client.

FieldTypeRequiredDefaultPurpose
enabledboolnofalseRegister the Podman runtime. Must be true for Ring to use Podman. When true and the socket is unreachable at startup, Ring fails fast
hoststringnorootless-first resolutionPodman API socket. Default resolution: RING_PODMAN_HOSTDOCKER_HOSTunix:///run/user/$UID/podman/podman.sockunix:///run/podman/podman.sock. Start it with systemctl --user start podman.socket (rootless)
use_host_registry_authboolnofalseAuthorize host-resolved registry credentials (see host registry auth)
host_registry_configstringnounsetExplicit path to the host registry config. Podman's login writes to containers/auth.json, so point at it here when the default Docker resolution doesn't pick it up

[server.runtime.containerd]

containerd speaks its own native gRPC API on a Unix socket, with no Docker daemon in between. CNI plugins (/opt/cni/bin) must be present for container networking.

FieldTypeRequiredDefaultPurpose
enabledboolnofalseRegister the containerd runtime. Must be true for Ring to use containerd. When true and the socket doesn't answer a Version round-trip at startup, Ring fails fast
socketstringno/run/containerd/containerd.sockPath to the containerd gRPC Unix socket (the stock location used by containerd, k3s and RKE2)
namespacestringnoringcontainerd metadata namespace Ring creates its images, snapshots, containers and tasks under. Keeps Ring's objects from colliding with k8s.io, moby or default on a shared host. This is containerd's own partition concept, unrelated to a Ring deployment namespace
use_host_registry_authboolnofalseAuthorize host-resolved registry credentials (see host registry auth). containerd has no login of its own; tools like nerdctl write to ~/.docker/config.json, the default this resolves
host_registry_configstringnounsetExplicit path to the host registry config

Host registry auth

use_host_registry_auth lets a deployment pull private images using the credentials the operator already configured on the host (e.g. via docker login), instead of inlining server/username/password in the manifest, which would otherwise be stored in cleartext in the database and returned by the API.

It is a deliberate two-flag handshake:

  1. The server authorizes it per runtime with use_host_registry_auth = true.
  2. The deployment activates it with config.use_host_auth: true (see manifest config).

Both are required: a deployment requesting host auth on a runtime that did not authorize it fails fast, with no silent fallback to an anonymous pull. The credential lookup honors credHelpers/credsStore. Set host_registry_config when the Ring daemon runs as a different user than the one who logged in (its ~ would otherwise resolve to the daemon's home, not yours).

[server.runtime.cloud_hypervisor]

FieldTypeDefaultPurpose
enabledboolfalseRegister the Cloud Hypervisor runtime. Must be true for Ring to use it. When true and binary_path can't be resolved at startup, Ring fails fast
binary_pathstringcloud-hypervisor (from $PATH)Absolute path to the cloud-hypervisor binary
firmware_pathstring$RING_CONFIG_DIR/cloud-hypervisor/vmlinuxPath to hypervisor-fw (the EFI firmware)
socket_dirstring$RING_CONFIG_DIR/cloud-hypervisor/socketsWhere Ring puts per-VM Unix sockets, console logs, volume shares
seccompstringunset (CH default: kill on violation)Forwarded to cloud-hypervisor --seccomp. Accepts "true", "false", "log". Set to "false" only on hosts where the kernel uses syscalls not whitelisted by CH (otherwise VMs die with SIGSYS)
max_console_log_bytesint10485760 (10 MiB)Size at which a per-VM console log is rotated. 0 disables rotation
max_console_log_backupsint3How many rotated backups (<id>.console.log.1, .2, …) to keep

[server.runtime.firecracker]

FieldTypeDefaultPurpose
enabledboolfalseRegister the Firecracker runtime. Must be true for Ring to use it. When true and binary_path can't be resolved at startup, Ring fails fast
binary_pathstringfirecracker (from $PATH)Absolute path to the firecracker binary
kernel_pathstring$RING_CONFIG_DIR/firecracker/vmlinuxPath to the uncompressed kernel image. Firecracker boots a kernel directly, so there is no firmware step
socket_dirstring$RING_CONFIG_DIR/firecracker/socketsWhere Ring puts per-VM API sockets and per-instance rootfs copies
boot_argsstringconsole=ttyS0 reboot=k panic=1 pci=offKernel command line passed to every microVM
max_console_log_bytesint10485760 (10 MiB)Size at which a per-VM console log is rotated. 0 disables rotation. Firecracker rotates by copy-truncate (it holds the log by inode), so the live file keeps its path across rotations
max_console_log_backupsint3How many rotated backups (<id>.console.log.1, .2, …) to keep

Examples

Minimal single-host (Docker)

[contexts.default]
current = true
host = "127.0.0.1"

api.scheme = "http"
api.port = 3030

[server.runtime.docker]
enabled = true

Without an enabled runtime Ring refuses to start, so the [server.runtime.docker] block is the minimum to get a working server on a Docker host.

Production with Docker + Cloud Hypervisor and TLS-fronted API

[contexts.default]
current = true
host = "0.0.0.0"

api.scheme = "https"                       # because nginx in front terminates TLS
api.port = 3030
api.cors_origins = ["https://dashboard.example.com"]

[server.scheduler]
interval = 5

[server.runtime.docker]
enabled = true
host = "unix:///var/run/docker.sock"

[server.runtime.cloud_hypervisor]
enabled = true
binary_path = "/usr/local/bin/cloud-hypervisor"
firmware_path = "/var/lib/ring/hypervisor-fw"
socket_dir = "/var/lib/ring/cloud-hypervisor/sockets"

Multiple contexts (workstation talking to remote servers)

[contexts.local]
current = true
host = "127.0.0.1"
api.scheme = "http"
api.port = 3030

[contexts.staging]
current = false
host = "ring-staging.example.com"
api.scheme = "https"
api.port = 443

[contexts.production]
current = false
host = "ring-prod.example.com"
api.scheme = "https"
api.port = 443

Switch context per command:

ring deployment list --context staging
ring apply -f api.yaml --context production

What auth.json is

Sitting next to config.toml, auth.json stores the bearer tokens that ring login generated. One entry per context:

{
  "local":      { "token": "eyJ..." },
  "staging":    { "token": "eyJ..." },
  "production": { "token": "eyJ..." }
}

Mode should be 0600. The file is created and updated by ring login; you generally don't edit it by hand.

See also