Deployment

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.

ChartValue (as of writing)
nameductor
version0.1.0
appVersion1.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.test

This 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:

ProbePathNotes
startupProbeGET /livezProcess-only; no dependency coupling
readinessProbeGET /readyRemoves a pod from service during dependency failure
livenessProbeGET /livezProcess-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).

LevelSettingDefault
PodrunAsNonRoottrue
PodrunAsUser / runAsGroup / fsGroup1000
PodseccompProfile.typeRuntimeDefault
ContainerallowPrivilegeEscalationfalse
ContainerreadOnlyRootFilesystemtrue
Containercapabilities.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:

SecretChart-managed keyInjected as
DatabaseDATABASE_URLDUCTOR_DATABASE_URL
RedisREDIS_URLDUCTOR_CACHE_URL
ConnectorDUCTOR_CONNECTOR_ENCRYPTION_KEYsame
Runtime configDUCTOR_CONFIGDUCTOR_CONFIG
Stripe (optional)STRIPE_API_KEY, STRIPE_WEBHOOK_SECRETcorresponding 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.

success failure helm install / upgrade pre-install / pre-upgrade Jobductor migrate up Roll new pods Block releaseJob retained for inspection
kubectl logs job/ductor-migrate
kubectl describe job/ductor-migrate

Rollback 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=production and config.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.metricsHost is a loopback address (enterprise metrics bind loopback).

Two constraints trip people up most often:

  • serviceAccount.create must be false in production. Helm cannot create an ordinary chart ServiceAccount before a pre-install hook, so the account must already exist. Set serviceAccount.name to 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=true is 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-runtime

Note 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: 10s

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