Skip to main content
This chapter describes how to deploy Decision Control Tower on a Kubernetes cluster (vanilla upstream Kubernetes 1.26+) or Red Hat OpenShift (4.12+). The reference manifests target a single namespace per environment (e.g. tower-prod) and assume PostgreSQL is available either inside the cluster or as a managed service reachable from pods.

Architecture overview

A standard production install consists of an Ingress / Route, a Tower Service backed by the Tower Deployment, a Tower Sidecar Deployment behind its own Service, and one PostgreSQL database per component: Tower Kubernetes / OpenShift deployment architecture Both Tower and the Sidecar are stateless containers; durability is provided by the two PostgreSQL databases. The Sidecar component handles process / case / job state for workflow features and is reached by Tower over the cluster’s pod network. See chapter 2 for database provisioning details.

Required resources and dependencies

Reference manifests

The manifests below use literal values for clarity. In practice they are commonly templated via Helm, Kustomize or a deploy script that performs envsubst before kubectl apply. Variables that vary per environment are called out as <placeholder>.

Namespace

ConfigMap (non-sensitive configuration)

Secret

For demonstration the manifest below uses stringData; in real deployments the values are loaded from a secret manager (Vault, Sealed Secrets, External Secrets Operator).

ConfigMap — environments.json seed (optional)

The Tower seeds the environments table on every startup from a JSON file. The seeder upserts by id, so editing the file and rolling the Deployment is the right way to update environment URLs after the initial bootstrap.

Field reference

The seed schema uses kebab-case keys (Jackson PropertyNamingStrategies.KEBAB_CASE): The enabled column is set to true automatically on insert. To temporarily disable an environment without deleting it, edit the row directly in the database.

Deployment

Service

Ingress (vanilla Kubernetes / Traefik)

If you use the cert-manager operator, annotate the Ingress accordingly:

Sidecar component (required)

Tower requires the Sidecar container for its core workflow features — process state, data index queries, jobs, and audit-trail SVG rendering. A Tower deployment without a matching Sidecar will start and accept logins, but workflow-driven endpoints will silently no-op. Deploy both together. A minimal sidecar deployment in the same namespace:
The shared callback secret must be identical on both sides — the Sidecar attaches it to outbound callbacks; Tower verifies it on inbound. Both Tower and the Sidecar expose the value under ALETYX_OIDC_CALLBACK_SECRET. Rotation requires updating both Secrets and rolling both Deployments. Once both are running, point Tower at the Sidecar Service in the Tower ConfigMap:
Use the in-cluster Service hostname — never a public ingress URL, which would add latency and a failure surface to what is a same-cluster call.
The Sidecar image expects BPMN process diagrams (*.svg) at ${KOGITO_SVG_FOLDER_PATH} (default /svgs) for the audit-trail UI to render process pictures. If you ship custom workflows, mount a ConfigMap containing the rendered SVGs at that path. Refer to the Sidecar package documentation for the full list.

Database connectivity

For an in-cluster PostgreSQL, refer to it via the standard service DNS pattern (<service>.<namespace>.svc.cluster.local). For a managed database (Cloud SQL, RDS, Azure Database), use the host endpoint directly. In both cases:
  • Ensure pods have outbound access to the database. On clusters with default-deny NetworkPolicy, add a rule that permits Tower pods to the database service / IP range.
  • Place the password in the tower-secret. Never embed it in the URL or in the ConfigMap.
  • For TLS connections, append the standard JDBC parameters to the URL (?ssl=true&sslmode=require).
If you operate a database-as-code controller (e.g. CrunchyData PGO, Zalando postgres-operator), Tower’s database can be provisioned alongside the application; the only requirement is that the user owns the database (see chapter 2).

OpenShift-specific considerations

Routes vs. Ingress

OpenShift supports both. Most OpenShift teams prefer Route, which is the native primitive and handles TLS termination at the cluster router automatically:
If your team standardizes on Ingress (e.g., behind a shared ingress controller), the manifest from the Kubernetes section works on OpenShift unchanged provided the cluster has an IngressClass for that controller.

Security Context Constraints (SCC)

The Tower image runs as nonroot and tolerates a read-only root filesystem with /tmp as emptyDir. This satisfies the OpenShift default restricted-v2 SCC without modification. No SCC binding is required.

Image streams

If your team uses ImageStreams to pin and promote images across environments, a typical setup is:
Reference it in the Deployment via the image-lookup annotation:
Beyond image management, no OpenShift-specific code paths exist in the application.

Persistent volumes

Tower itself does not require persistent storage — all state is in PostgreSQL. The Sidecar component writes only to /tmp and PostgreSQL. If you mount the seed JSON via PVC instead of ConfigMap (rare, mostly relevant for very large environment lists), use accessModes: [ReadOnlyMany] to allow scaled deployments.

High availability

Running multiple Tower replicas is supported provided:
  1. The OIDC flow uses stateless tokens (default with both Entra and Keycloak — no session affinity required).
  2. The shared callback secret is identical on every replica (it is, since it lives in a Secret).
  3. The Decision Control health probes are tolerant of being run by multiple replicas (they are; each replica probes independently).
To scale:
Add a PodDisruptionBudget to keep at least one replica during voluntary disruptions:

Operational commands

Diagnostic patterns

For diagnostics that require a shell, the Tower image is distroless and ships no shell of its own. Use kubectl debug with a container image your organization trusts (an ephemeral debug container targeting the Tower pod), or run the diagnostic command from a separate pod in the same namespace.

Troubleshooting

Cannot log in

  1. Verify the OIDC provider’s registered redirect URI matches ${APP_HOST}/auth/callback exactly — including the scheme (https://) and any trailing path.
  2. Tail the Tower pod logs while attempting to log in:
  3. Confirm the OIDC provider is reachable from inside the cluster (network policies, firewall, DNS).

Environments not loading

The Tower and Sidecar images are distroless and ship no shell, so kubectl exec is not available. Use kubectl run to launch a short-lived helper pod in the same namespace, or kubectl debug --copy-to with a tools image, and run the checks from there.
  1. Verify the Tower Sidecar Deployment is 1/1 Running — Tower depends on it for environment state. If the Sidecar is missing or crash-looping, the environments page surfaces no rows.
  2. From a helper pod in the same namespace, check the Sidecar Service is reachable at the URL Tower uses (the value of ALETYX_DC_TOWER_SIDECAR_URL):
  3. Verify the Sidecar can reach its database, again from a helper pod:
  4. The environments table is seeded from /app/config/environments.json on first boot. If the table is empty in an existing deployment, confirm the seed file is mounted (see chapter 4 — ConfigMap environments.json seed).

Tasks not appearing

  1. Verify the signed-in user holds a role in ALETYX_OIDC_RBAC_ROLES_ALLOWED_* for ACCESS or the more specific action — empty allow-lists deny everything when RBAC is enabled.
  2. Confirm the underlying workflow ran and produced tasks: the Sidecar’s Data Index logs show task creation events.
  3. Tail the Tower pod logs for errors emitted by the task-fetch loop. Slow / unreachable Sidecar HTTP shows up as timeouts.