Skip to content
docuconf
docuconf on GitHub

Inputs

An application reads its configuration from two places: environment variables and files. A contract describes both. For each input it records the type, the constraints, whether it is required or secret, and — because libraries disagree — exactly how the app parses it.

The platform supplies each input from a source: a literal, a Kubernetes reference, an injector, a mounted file or a config-file overlay. Whatever the source, the value the app finally sees is checked twice: by the platform before deploy where it can be, and by the SDK at boot always.

Environment variables

vars is a map keyed by the variable name, which must match ^[A-Z][A-Z0-9_]*$. SDKs map idiomatic field names (databaseUrl, DatabaseUrl) to this form.

Variable types

The type set is closed in v1alpha1: a new type needs a spec change, because every SDK must parse it identically.

Variable types
TypeMeaningConstraint fieldsPlatform valueWire form
stringFree text.minLength, maxLength (in characters), pattern (RE2, matches anywhere unless anchored)stringas is
int64-bit signed integer. SDKs export narrower host ranges as min/max.min, maxintbase-10, no leading + or zeros: 8080
floatFinite decimal number, parsed independently of locale.min, maxnumbershortest round-trip decimal: 0.5
booltrue or false, case-insensitive.—booltrue / false
durationA length of time, written in Go syntax on the platform side.min, max (durations), encodingGo-syntax duration: 1m30sper encoding: go, iso8601, seconds or timespan
urlAn absolute URL with a scheme.schemes, maxLength (the URL as it is)string with scheme://as is
enumOne of a fixed set of strings.values (non-empty)one of valuesas is
listA list of strings or integers.items (string | int), encoding, separator, minItems, maxItems, itemMin and itemMax (int items), itemMinLength and itemMaxLength (string items, after splitting)listper encoding: csv, json or indexed
jsonA structured value, checked against a JSON Schema generated from the app’s own type.schema (JSON Schema), maxLength (the wire string)any JSON valuecompact JSON
  • pattern is RE2 and matches anywhere in the value; anchor it with ^…$ to match the whole value. RE2's \d, \w, \s and \b are ASCII-only.
  • json variables and config files carry a JSON Schema generated from the app's own type, so the platform checks structured values against the type the app deserializes into.
  • An empty string is a value for string. For every other type it means unset.

Length limits

  • Lengths count characters, meaning Unicode code points, never bytes or UTF-16 code units: 日本 is 2 characters, and ZÜ01 fits an itemMaxLength of 4. CUE's strings.MaxRunes and JSON Schema's maxLength count the same way, so the platform and every SDK agree.
  • minLength and maxLength bound a string, and maxLength also a url (the URL as it is) or a json value.
  • maxLength on a json value bounds its wire string: before deploy, the compact JSON the renderer writes; at boot, the raw value the app receives, whitespace included, before it is parsed. The Helm values schema cannot express it, so docuconf vet and the SDK check it.
  • itemMinLength and itemMaxLength bound each item of a string list, after the list is split, so a csv separator never counts. They are not allowed on an int list, as itemMin and itemMax are not allowed on a string list.
  • A value outside its limits is out_of_range at boot. A too-long secret is reported by its length, never its value.
  • An app that stores values in fixed-width byte fields, such as a COBOL PIC X(n), should declare a limit that leaves room for multi-byte characters, or restrict values to ASCII with a pattern such as ^[ -~]*$.

Fields on every variable

Variable fields
FieldRule
typeOne of the variable types. The set is closed in v1alpha1.
descriptionRequired. Plain text, at least 5 characters: what the input is, in one phrase. Rendered into generated docs.
detailsOptional. CommonMark: why the input exists and when to change it. Not blank, at most 4000 characters. Used only in generated docs, never at runtime.
requiredDefault false. A required variable cannot have a default.
defaultMust satisfy the variable’s own constraints (checked at declaration time).
secretThe value must come from a secretKeyRef. No default, no examples, never printed.
groupFree-form grouping for docs.
examplesExample values, as strings, for docs.
deprecated{message, replacedBy?}. SDKs warn at boot when it is set.
configKeyThe app’s own config key (Orders:Timeout, orders.timeout), for docs.

Value sources

A value is usually a literal. Some only exist in the cluster or are supplied at runtime, so the platform may give a reference instead:

Value sources
SourceExampleAllowed forCheckedStatus
literalPORT: 9090every non-secret typebefore deployImplemented
configMapKeyRef{configMapKeyRef: {name: "limits", key: "rate"}}every non-secret type except listat bootImplemented
fieldRef (Downward API){fieldRef: {fieldPath: "metadata.namespace"}}stringalways validImplemented
resourceFieldRef{resourceFieldRef: {resource: "limits.memory"}}intalways validImplemented
secretKeyRef{secretKeyRef: {name: "db", key: "url"}}secret variables (one of the two secret sources)at bootImplemented
injected, with a reference{injected: {provider: "bank-vaults", ref: "vault:secret/data/db#url"}}every type, including secrets; rendered verbatim as the env value for the injector to resolvereference shape before deploy; the value at bootImplemented
injected, without a reference{injected: {provider: "otel-operator"}}every type, including secrets; not rendered: a webhook, init process or operator sets itat bootImplemented

A secret variable is never a literal. It comes from a secretKeyRef or from an injector, and its content is checked only at boot. No docuconf tool ever prints a secret value.

Cloud-native injection

Many platforms do not put configuration into the pod spec at all. A mutating webhook, an init process or a wrapper resolves secrets at startup and hands the app a ready environment, or writes files the app reads. docuconf supports this by design:

  1. The SDK validates the process as it actually starts. Configuration is read from the real process environment and filesystem at startup, after any injector has run. A value from Bank-Vaults or op run is checked exactly like one from the pod spec.
  2. The contract does not care who supplies a value. It describes what the app accepts. The platform's values document says who supplies it.
  3. The platform declares injected inputs as injected, naming the provider and, when the injector reads a reference from the env value, that reference. docuconf vet checks the reference's shape, not the secret, and renders it verbatim for the injector to resolve.
# Platform values for orders-api
DATABASE_URL:
  injected: {provider: bank-vaults, ref: "vault:secret/data/orders/db#url"}
OTEL_EXPORTER_OTLP_ENDPOINT:
  injected: {provider: otel-operator}        # set by the operator; not rendered
LOG_LEVEL: warn                               # an ordinary literal
Runtime injectors
InjectorWhat it doesHow the contract describes it
Bank-Vaults (vault-env webhook)Env values like vault:secret/data/db#url are resolved by vault-env, which then execs the app with real values.injected value source with ref; the SDK sees and validates the resolved value.
Vault Agent injectorWrites secrets as files under /vault/secrets, rendered from templates.injected file source, with the pod annotations that make the agent write the file at its declared path; the SDK checks the file at boot like any other.
External Secrets OperatorSyncs a cloud secret manager into a Kubernetes Secret.secretKeyRef or secret file source, as usual; nothing special.
Secrets Store CSI driverMounts secrets from Vault, AWS, Azure or GCP as files.csi file source.
1Password, Doppler, Infisical and similar wrappersA wrapper process (op run, doppler run) resolves references and execs the app.injected value source, with the wrapper’s reference format as ref.
Operators and webhooks (OpenTelemetry, service meshes)Add variables such as OTEL_EXPORTER_OTLP_ENDPOINT to the pod.injected value source without ref, so the platform knows who supplies it, with the pod annotation or label that switches the injector on.

SDKs validate injected values like any other, because they read the process environment at startup, after injection. If a secret still holds a reference (vault:, op://, ref+), the injector did not run, and the SDK fails with invalid_type, naming the variable and the reference scheme but never the value.

Enabling the injector

Most injectors are switched on by annotations or labels on the pod. An injected source, for a variable or a file, may carry them as podAnnotations and podLabels, maps of strings. The values document may also have its own podAnnotations and podLabels, beside the variables, for settings every injected input shares, such as the Vault role. The contract does not change: which injector runs is a fact about the cluster.

  • Placeholders let an annotation that names one input be written once, beside it: {input} is the variable's or file's name, and for a file {path} is its path, {dir} the directory it occupies and {file} its file name. Nothing else is expanded, so a Vault Agent template's {{ .Data }} stays as it is. A placeholder the source does not define ({path} on a variable, any placeholder in the shared maps) is an undefinedPlaceholder error.
  • Rendering. docuconf render merges the shared maps and every injected input into two maps, podAnnotations and podLabels, for the pod template's metadata: webhooks see pods, not Deployments. In Helm, the library chart's docuconf.podAnnotations and docuconf.podLabels helpers return the same maps. Keys must be Kubernetes qualified names and label values valid label values, checked after expansion.
  • Conflicts. The same key with the same value from several inputs is fine. The same key with different values is a conflictingPodAnnotation or conflictingPodLabel error naming the key and both sources, in docuconf vet, in the render and in the Helm helpers.
  • Not secret. Anyone who can read the pod can read its annotations, so they hold references and settings (a Vault path, a role, a template), never secret material.

The Vault Agent injector writing a secret config file db-creds, declared at /vault/secrets/db.json:

# values.yaml
podAnnotations:                      # shared: one agent per pod, one role
  vault.hashicorp.com/agent-inject: "true"
  vault.hashicorp.com/role: ledger
# files.yaml
db-creds:
  injected:
    provider: vault-agent
    podAnnotations:
      vault.hashicorp.com/agent-inject-secret-{input}: database/creds/ledger
      vault.hashicorp.com/agent-inject-template-{input}: '{{- with secret "database/creds/ledger" -}}{{ .Data | toJSON }}{{- end }}'
      vault.hashicorp.com/secret-volume-path-{input}: "{dir}"
      vault.hashicorp.com/agent-inject-file-{input}: "{file}"

renders the two shared annotations plus agent-inject-secret-db-creds, agent-inject-template-db-creds, secret-volume-path-db-creds: /vault/secrets and agent-inject-file-db-creds: db.json, so the agent writes the file exactly at the declared path. Values are strings: write "true", not true. See SPEC.md §4.5.2 for Bank-Vaults, the OpenTelemetry operator and the edge cases.

Files

files is a map keyed by input name, a DNS label because it names a volume.

File types

File input types
TypeContentConstraint fieldsSecret
configA structured config file in format json, yaml or toml.format, schema (JSON Schema generated from the type the app binds the file to)optional
tlsA key pair in the kubernetes.io/tls layout: tls.crt, tls.key, and ca.crt when requireCA is set.dnsNames, keyAlgorithms (RSA, ECDSA, Ed25519), minRemaining, requireCAalways
caBundleOne or more PEM CA certificates.minCertificates (default 1)optional
keystoreA PKCS#12 or JKS keystore.format, passwordVar (a declared secret variable)always
textA text file, such as a licence key.pattern (RE2), minLength, maxLengthoptional
binaryOpaque bytes, such as a GeoIP database.maxSize onlyoptional

Fields on every file input

File input fields
FieldRule
typeOne of the file types.
description, details, required, group, deprecatedAs for variables.
secretContent must come from a secret store. Forced true for tls and keystore.
pathWhere the app reads it: a directory for tls, a file otherwise. Absolute.
pathEnvA variable the platform sets to path (SSL_CERT_FILE). Not also declared in vars.
reloadrestart (default): a changed source rolls the pods. watch: the app reloads it.
maxSizeUpper bound in bytes.

Mount rules, enforced by the meta-schema: a file is mounted at its parent directory, and a TLS pair at its own directory; no two inputs share a directory; none is mounted over a reserved directory such as /, /etc, /etc/ssl/certs, /usr, /var or /app; files are projected with items, never subPath, so rotation reaches them; secret files are mode 0400, others 0444.

File sources

File sources
SourceForChecked before deployStatus
inlinenon-secret filesEverything: format, schema, pattern, size, certificate count. Rendered as an immutable, content-hashed ConfigMap. Config files may be given as structured data.Implemented
configMapnon-secret filesThat a key is given for single files.Implemented
secretanyKey presence; with resolved metadata, the Secret’s type (kubernetes.io/tls) and its keys.Implemented
certificate (cert-manager)tlsFrom the Certificate spec: covers dnsNames, uses an allowed key algorithm, renewBefore ≥ minRemaining.Implemented
csi (Secrets Store CSI driver)anyNothing; checked at boot.Implemented
image (image volume)non-secret filesNothing; for data above the 1 MiB ConfigMap limit.Implemented
injected (Vault Agent and similar)anyThat the injector is named, and its pod annotations and labels; no volume is rendered, the injector writes the file at path. Checked at boot.Implemented

Resolved fields — a Secret's type and keys, a Certificate's spec — are metadata the platform tooling reads from the cluster. They are never secret contents. When absent, those checks move to boot.

Rotation. reload: watch means the app rereads the file, so the platform does nothing. reload: restart makes the platform roll the pods when the source changes. Inline content is content-hashed, so it always rolls the pods.

Config-file overlays

Hosts such as .NET, Spring Boot and Rails layer configuration files under environment variables. A common cloud-native pattern keeps the app's baked-in files and adds one more file, mounted by the platform, between them and the environment:

var builder = WebApplication.CreateBuilder(args);

// 1. Baked into the image: defaults and per-environment values.
builder.Configuration.AddJsonFile("appsettings.json", optional: false);

// 2. Mounted by the platform from a ConfigMap, reloaded on change.
builder.Configuration.AddJsonFile("/app/config/appsettings.Production.json", optional: true, reloadOnChange: true);

// 3. Environment variables (including injected secrets) win.
builder.Configuration.AddEnvironmentVariables();

A contract describes the overlay, and the platform renders it:

overlays: platform: {
	format: "json"
	path:   "/app/config/appsettings.Production.json"
	reload: "watch"   // reloadOnChange: true
}
  • Precedence is fixed and the same on every host: baked-in base file, then profile file, then the platform overlay, then environment variables.
  • The platform routes variables to the overlay in its values document. docuconf render writes each value at the variable's configKey (Orders:CheckoutTimeout becomes {"Orders": {"CheckoutTimeout": …}}) in native types — numbers, booleans, arrays — rather than env-var strings, and ships the file as a ConfigMap mounted at path.
  • Validation is identical: overlay values are typed values checked by #Validate, the same as env values. Secrets never go in an overlay, because overlays are ConfigMaps.
  • Overlays add, never replace. The overlay sits at its own path, so the app's baked-in appsettings.Production.json and its values still apply. Mounting over the baked-in file would silently discard them, and the meta-schema rejects an overlay path that is also a baked-in config file.
  • reload: watch maps to the host's reload-on-change, so the app sees new values without a restart (in .NET, through IOptionsMonitor<T>). restart rolls the pods instead.
Hosts that layer config files
HostBaked-in filesPlatform overlayReload
.NET (Microsoft.Extensions.Configuration)appsettings.json, appsettings.{Environment}.jsonAddJsonFile("/app/config/appsettings.Production.json", optional: true, reloadOnChange: true)IOptionsMonitor<T> sees changes; IOptions<T> needs a restart
Spring Bootapplication.yml, application-{profile}.ymlspring.config.additional-location=file:/app/config/restart, or Spring Cloud refresh
Rails (anyway_config)config/<name>.ymlan extra YAML file read before envrestart
Rust (figment)Config.toml with profilesToml::file("/app/config/Config.toml") merged before Envrestart
Kotlin (Hoplite)application.conf / .yamlan extra property source ahead of the defaultsrestart, or Hoplite reloadable config

docuconf render and the Helm library chart write the overlay's ConfigMap, volume and mount. The SDKs for hosts that layer files load it in the right place; see the SDK table.

Wire encodings

Environment values are strings. The platform always holds typed values — lists as lists, durations in Go syntax — and the renderer writes each one in the form the app's library parses. For most types every library agrees. Lists and durations differ, so the contract records the app's encoding:

List encodings
List encodingWire formNative to
csv (default)a,b joined by separatorcaarlos0/env, Spring Boot, anyway_config
json["a","b"]pydantic-settings
indexedNAME__0=a, NAME__1=bMicrosoft.Extensions.Configuration
Duration encodings
Duration encodingWire form for 90sNative to
go (default)1m30sGo time.ParseDuration
iso8601PT1M30Spydantic timedelta, ActiveSupport::Duration, java.time.Duration (Spring)
seconds90anything that takes a number
timespan00:01:30.NET TimeSpan.Parse

Renderers double every $ in a literal ($ becomes $$), because Kubernetes expands $(NAME) in env values. Platform authors never see encodings: they write "90s" and ["a", "b"] for every app.

Profiles

Values in files the image ships with are part of what the app runs with, so the contract includes them. A value in an always-loaded base file (appsettings.json, application.yml) is a default. A value in a profile file (appsettings.Staging.json) goes in profiles.defaults, applied when the selector variable (ASPNETCORE_ENVIRONMENT, SPRING_PROFILES_ACTIVE, RAILS_ENV) selects that profile. A required variable is satisfied when the platform or the selected profile supplies it. A secret may never have a value in any file.