Skip to content

Configuration

Dploy has two configuration surfaces:

  • The API is configured with environment variables (set via the Helm chart’s ConfigMap and Secret).
  • The operator reads a cluster-scoped OperatorConfig custom resource for cluster-wide defaults.
VariableDescription
JWKS_URLJWKS endpoint used to validate JWT signatures
JWT_ISSUERExpected JWT iss claim
VariableDefaultDescription
JWT_AUDIENCEdployExpected JWT aud claim
JWT_USERNAME_CLAIMnameClaim used as the username
FORWARDED_CLAIMSsub,preferred_username,email,name,groupsThe only claims copied out of the token into a DployInstanceClaim, where templates read them as .Params. Everything else stops at the API server
OIDC_ISSUER$JWT_ISSUEROIDC issuer (internal, for token exchange)
OIDC_PUBLIC_ISSUER$OIDC_ISSUEROIDC issuer used for browser redirects
OIDC_CLIENT_IDdployOIDC client ID
OIDC_CLIENT_SECRETdploy-secretOIDC client secret
OIDC_REDIRECT_URLhttp://localhost:8080/auth/callbackOIDC callback URL
VariableDefaultDescription
DPLOY_NAMESPACEdploy-systemNamespace where the dploy CRs live
MAX_ENVIRONMENTS_PER_USER5Displayed quota. The cap the operator actually enforces comes from OperatorConfig.defaults.maxInstancesPerUser (or the template’s override)
DEFAULT_TTL86400Fallback initial TTL in seconds, for display. The operator resolves the real one
EXTEND_TTL7200Fallback extension granted per /extend
VariableDefaultDescription
SERVER_HOST0.0.0.0Bind address
SERVER_PORT8080Port
DEBUGfalseVerbose logging

The operator reads a single cluster-scoped OperatorConfig named default. Any other name is ignored. It carries cluster-wide defaults that individual DployTemplates can override per-template.

apiVersion: dploy.dev/v1alpha1
kind: OperatorConfig
metadata:
name: default
spec:
# GitOps engine used when a template does not override it.
defaultEngine: flux # only "flux" is implemented today
# Defaults for the Flux engine.
flux:
namespace: flux-system # where HelmRelease + Sources are reconciled
serviceAccountName: "" # optional: SA the helm-controller impersonates
interval: 5m # GitRepository / HelmChart poll interval
# Used to build the default per-instance hostname: <name>-<uid>.<baseDomain>.
baseDomain: env.dploy.dev
# Cluster-wide default Go (text/template + sprig) for an instance's public URL.
# Overridable per template via DployTemplate.spec.connectionURLTemplate.
# Empty falls back to "https://<Host>".
connectionURLTemplate: "http://{{ .Host }}"
# Cluster-wide default presentation: "web" (clickable URL, redirect on ready)
# or "instructions" (copyable command, no redirect — see connectionMessageTemplate).
defaultConnectionType: web
# Cluster-wide default instructions template. Rendered with the same context as
# connectionURLTemplate plus .URL / .ConnectionURL (the resolved target).
# Only used when connectionType is "instructions".
connectionMessageTemplate: "" # e.g. "ssh root@{{ .ConnectionURL }} -p 22000"
# Applied to instances when their template omits the value.
defaults:
ttlSeconds: 86400 # initial TTL (24 h)
extendSeconds: 7200 # TTL bonus per /extend call (2 h)
maxExtends: 3 # -1 = unlimited; unset falls back to 3
maxInstancesPerUser: 5 # per-owner quota
# Free-form map exposed to value templates as `.Config.Values`. Anything you
# want common across templates (image registries, organisation toggles, …).
values:
registry: ghcr.io/your-org
ingressClass: nginx
FieldDescriptionDefault
defaultEngineGitOps engine: flux only today (argocd reserved)flux
flux.namespaceNamespace where the helm-controller reconciles HelmReleasesflux-system
flux.serviceAccountNameSA the helm-controller impersonates (optional)—
flux.intervalPoll interval for GitRepository / HelmChart sources5m
baseDomainBase domain for the fallback <name>-<uid>.<baseDomain> host—
connectionURLTemplateCluster default URL template (overridable per template)https://<Host>
defaultConnectionTypeweb (clickable) or instructions (copyable command)web
connectionMessageTemplateCluster default instructions template (when instructions)—
defaults.ttlSecondsDefault TTL for a new instance86400
defaults.extendSecondsSeconds added per /extend call7200
defaults.maxExtendsMaximum extensions allowed (-1 = unlimited)3
defaults.maxInstancesPerUserPer-owner quota (sanitized owner key)5
valuesFree-form map exposed as .Config.Values{}

connectionURLTemplate, connectionMessageTemplate (in OperatorConfig or per template) and valuesTemplate (per DployTemplate) are all rendered with Go text/template + sprig against this data:

VariableTypeNotes
.OwnerstringSanitized owner key — empty for unclaimed pool members and forbidden in pool valuesTemplate (CEL-enforced anonymity)
.UUIDstring8 hex chars, generated once by the operator, immutable in status.uuid
.BaseDomainstringCluster baseDomain
.HoststringPrecomputed <name>-<uid>.<baseDomain> — routing-neutral hostname
.URL / .ConnectionURLstringSet after connectionURLTemplate renders; only available in valuesTemplate and connectionMessageTemplate
.NamespacestringThe instance’s workload namespace
.Template*DployTemplateFull template spec, useful for {{ .Template.Name }} / {{ .Template.Spec.X }}
.Paramsmap[string]stringThe whole request context: the params the template declares, merged with the claims listed in FORWARDED_CLAIMS. The raw token never reaches the cluster (nil in pool — anonymity)
.Config.Valuesmap[string]anyOperatorConfig.spec.values

Overriding connectionURLTemplate per template

Section titled “Overriding connectionURLTemplate per template”

A DployTemplate.spec.connectionURLTemplate wins over the cluster default. Examples:

# Owner-based URL (leaks identity but useful for "your own session" UIs).
connectionURLTemplate: "https://{{ .Owner }}-{{ .UUID }}.{{ .BaseDomain }}"
# Path-based instead of subdomain (single ingress hostname, multiple instances).
connectionURLTemplate: "https://shared.example.com/{{ .UUID }}/"
# Custom port + scheme.
connectionURLTemplate: "https://{{ .Host }}:8443"

When a template (or the operator default) sets connectionType: instructions, the API doesn’t redirect on ready — it surfaces status.connectionMessage so the UI can show a copyable command:

# OperatorConfig (or DployTemplate.spec):
defaultConnectionType: instructions
connectionMessageTemplate: "ssh root@{{ .ConnectionURL }} -p 22000"
# … combined with a connectionURLTemplate that returns the bare host:
connectionURLTemplate: "{{ .Host }}"

Result: status.connectionMessage = "ssh root@vscode-abc123.example.com -p 22000", displayed by the UI without redirecting.

Usernames extracted from the JWT are sanitized for Kubernetes compatibility:

  • lowercased
  • . and @ replaced with -
  • any remaining non-[a-z0-9-] characters removed
  • leading/trailing - trimmed
John.Doe@example.com → john-doe-example-com
ResourcePatternExample (on-demand)Example (pool)
Workload namespace<owner>-<name>-<uid>john-doe-vscode-a1b2c3d4pool-webshell-c7218ff8
Default Host<name>-<uid>.<baseDomain>vscode-a1b2c3d4.env.dploy.devwebshell-c7218ff8.env.dploy.dev
DployInstance (on-demand)<owner>-<template>john-doe-vscode— (pool members get random suffixes: webshell-pool-XXXXX)
Flux source (per template)<template>-srcvscode-srcwebshell-src

The UUID is 8 hex characters, generated once by the operator at the first reconcile and stored immutably in status.uuid. The workload namespace uses pool as its owner segment for pool members, and keeps it after they are claimed — the namespace is created when the instance is provisioned, long before an owner exists, and a namespace cannot be renamed. Look at dploy.dev/owner on the instance, not at the namespace name, to see who holds a pooled environment; the Host template segment always reflects the DployTemplate name regardless of mode, so webshell-<uid> and kasm-<uid> are visibly distinct even before they’re claimed.