Kubernetes (Helm)
The deploy/helm/ductor chart — replicas, autoscaling, the migration hook, secrets, and the fail-closed production gate.
Ductor ships a Helm chart at deploy/helm/ductor. It runs Ductor as a
Deployment with autoscaling, a PodDisruptionBudget, a pre-install/pre-upgrade
migration Job, and an optional Prometheus ServiceMonitor — with a hard
enterprise-posture gate for production.
Chart and image versions below are the pinned values as of writing;
Chart.yaml and values.yaml are the live source.
| Chart | Value (as of writing) |
|---|---|
name | ductor |
version | 0.1.0 |
appVersion | 1.0.0 (default image tag) |
Install (development)
helm upgrade --install ductor deploy/helm/ductor \
--set postgres.host=timescaledb.example.test \
--set postgres.password=development-only \
--set redis.host=dragonfly.example.testThis is the production=false posture: the chart may create database and cache
Secrets from inline values and runs the development profile. It does not mount
a connector encryption key, so never store real connector credentials in it.
Key values
Prop
Type
One listener, two Service ports
The runtime serves REST, Connect, and the health routes from a single api
listener. Both the http and grpc Service ports target that same container
port — there is no separate gRPC listener.
Config and probes
The Deployment sets non-secret config from a ConfigMap and injects secrets as env vars. ConfigMap and Secret checksums are stamped as pod annotations, so a config change triggers a rolling restart automatically. Probes:
| Probe | Path | Notes |
|---|---|---|
startupProbe | GET /livez | Process-only; no dependency coupling |
readinessProbe | GET /ready | Removes a pod from service during dependency failure |
livenessProbe | GET /livez | Process-only, so a shared dependency outage doesn't restart every replica |
When tls.existingSecret is set (the enterprise path) the probes switch to
HTTPS, because the combined API listener terminates TLS. Enterprise probes
then require probes.host: kubelet's default Pod-IP Host header is rejected by
the enterprise allowed-hosts policy, so you must set an explicit probe hostname
and include that exact name in api.allowed_hosts inside DUCTOR_CONFIG, or
every probe fails and the pod never becomes ready.
Pod hardening
The chart ships defaults that satisfy the Kubernetes restricted Pod Security
Standard,
so a default install is admitted by a restricted-PSS namespace with no extra
tuning. The image runs as uid/gid 1000 (see docker/Dockerfile).
| Level | Setting | Default |
|---|---|---|
| Pod | runAsNonRoot | true |
| Pod | runAsUser / runAsGroup / fsGroup | 1000 |
| Pod | seccompProfile.type | RuntimeDefault |
| Container | allowPrivilegeEscalation | false |
| Container | readOnlyRootFilesystem | true |
| Container | capabilities.drop | [ALL] |
Both the application Deployment and the migration hook Job carry the same pod- and container-level contexts.
Read-only root filesystem needs a writable /tmp
Because readOnlyRootFilesystem is true, the chart mounts an emptyDir at
/tmp for scratch space — in both the Deployment and the <release>-migrate
Job. If you enable local archival (archival.backend=local), add a separate
writable volume for archival.storage_path; /tmp is scratch only.
Graceful shutdown
terminationGracePeriodSeconds defaults to 60. On termination the pod flips
readiness, waits shutdown_drain_delay (default 5s), then drains in-flight
work — so the grace period must exceed shutdown_drain_delay plus enough
headroom to finish that drain. A value inside the drain window gets pods
SIGKILLed mid-drain. If you raise shutdown_drain_delay, raise
terminationGracePeriodSeconds to match.
Secrets
The chart manages Secrets when you don't supply an existingSecret, or reads
your own when you do:
| Secret | Chart-managed key | Injected as |
|---|---|---|
| Database | DATABASE_URL | DUCTOR_DATABASE_URL |
| Redis | REDIS_URL | DUCTOR_CACHE_URL |
| Connector | DUCTOR_CONNECTOR_ENCRYPTION_KEY | same |
| Runtime config | DUCTOR_CONFIG | DUCTOR_CONFIG |
| Stripe (optional) | STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET | corresponding vars |
Redis secret key vs env var
The redis Secret key is REDIS_URL, but it is injected into the container as
DUCTOR_CACHE_URL — the names intentionally differ. Secret key names are
independent from the canonical runtime env names and can be overridden with the
matching *.existingSecretKey values.
Migrations run as a Helm hook
migrations.enabled (default true) renders a batch/v1 Job,
<release>-migrate, with Helm hooks pre-install,pre-upgrade. Its container runs
ductor migrate up, so under the chart migrations run before the new pods
roll — the application deliberately keeps database.auto_migrate off in
Kubernetes. Tune it with migrations.backoffLimit (default 1) and
migrations.activeDeadlineSeconds (default 600).
The Job is retained on failure: a failed migration blocks the release and stays available for inspection.
kubectl logs job/ductor-migrate
kubectl describe job/ductor-migrateRollback is operator-controlled, not automatic
Migration rollback is a deliberate database recovery, not an automatic Helm
rollback. Stop the rollout, inspect the retained Job, restore the pre-migration
database snapshot if needed, and only then roll back the chart revision. A Helm
rollback alone does not undo applied schema changes. GitOps operators that run
migrations separately may set migrations.enabled=false, but must run
ductor migrate up before application pods start.
The production gate
Setting production: true turns on a fail-closed guard rail: the chart
refuses to render unless the whole enterprise posture is in place. It is not
a tuning knob — it is a wall that keeps a half-hardened cluster from ever being
deployed. Rendering fails unless:
config.environment=productionandconfig.securityProfile=enterprise.- Database, Redis, the connector encryption key, API TLS, and the complete runtime config all come from pre-provisioned Secrets — no inline values.
config.metricsHostis a loopback address (enterprise metrics bind loopback).
Two constraints trip people up most often:
serviceAccount.createmust befalsein production. Helm cannot create an ordinary chart ServiceAccount before apre-installhook, so the account must already exist. SetserviceAccount.nameto a non-default, operator-pre-provisioned account; the Deployment and the migration Job both use that exact account, with API-token automount disabled and no RBAC granted.serviceMonitor.enabled=trueis rejected in production. Enterprise metrics bind to loopback, and a cluster ServiceMonitor cannot scrape a loopback listener directly — you need a secured in-pod metrics proxy instead.
config.existingSecret and connector.existingSecret must be the same Secret
Both must name the same Kubernetes Secret object, so the active connector
key and the DUCTOR_CONFIG document (which carries the matching
connector.encryption_key_id and decrypt-only connector.rotation_keys
keyring) can never be observed at different revisions.
Rolling pods when external Secrets change
The chart checksums the config it manages, but Helm cannot checksum the
contents of an external Secret. config.externalSecretsRevision is a
non-secret value you bump whenever a referenced external Secret changes; changing
it rolls the pod template so the new Secret contents are picked up. (Alternatively
run an explicit kubectl rollout restart after the coordinated Secret update.)
Enterprise install
helm upgrade --install ductor deploy/helm/ductor \
--set production=true \
--set config.environment=production \
--set config.securityProfile=enterprise \
--set config.existingSecret=ductor-runtime-config \
--set config.metricsHost=127.0.0.1 \
--set config.externalSecretsRevision=2026-07-11-1 \
--set probes.host=ductor-probe.internal \
--set postgres.existingSecret=ductor-database \
--set redis.existingSecret=ductor-cache \
--set connector.existingSecret=ductor-runtime-config \
--set tls.existingSecret=ductor-api-tls \
--set serviceAccount.create=false \
--set serviceAccount.name=ductor-runtimeNote config.existingSecret and connector.existingSecret naming the same
Secret (ductor-runtime-config). See the chart's README.md for the connector
key-rotation and HMAC-key procedures, and
Production concerns for the config behind these
switches.
ServiceMonitor (non-production)
Outside the enterprise posture, enable Prometheus Operator scraping of the metrics port:
serviceMonitor:
enabled: true
interval: 30s
scrapeTimeout: 10sUnscraped metrics fire no alerts
With serviceMonitor.enabled=false (the default) the pods still expose
/metrics on :9090, but nothing scrapes them — so Prometheus alert rules
never fire. The chart prints a post-install NOTES warning to this effect.
Either set serviceMonitor.enabled=true (if prometheus-operator's
ServiceMonitor CRD is installed) or point your Prometheus scrape config at the
metrics port directly.
See Observability for the metrics surface.