Deployment topologies
See what the DALP Helm charts deploy and how each backend role scales: three Platform API roles, PgBouncer in front of PostgreSQL, and the environment bundles.
A dalp Helm release runs the Platform API as three independently scaled Kubernetes Deployments, api, runner, and indexer, in front of a PostgreSQL database every role reaches through a connection pooler. Reach for this page when you are sizing a deployment: how far each role scales, what extra replicas cost at the pooler and the database, and which chart bundle fits your environment. It does not cover installing the chart the first time; see Prerequisites and Helm charts for that. It does not cover backup, restore, or multi-region failover; see Backup and disaster recovery for the shape a real outage takes.
One dalp release, three Platform API roles. Each role scales on its own autoscaler, and every role's queries reach PostgreSQL through PgBouncer, where pool limits are priced against the server's connection ceiling.
What deploys
The dalp Helm chart is the umbrella you deploy for one DALP InstanceA DALP Instance is one self-contained copy of the platform, deployed inside a boundary you choose and connected to ledger networks you approve.Read the concept, one self-contained copy of the platform running inside a boundary you choose and connected to the ledger networks you approve. It composes conditionally enabled subcharts. Five are on by default: dapi (the Platform API), dapp (the Console), docs (this documentation site), pgbouncer (the PostgreSQL connection pooler), and postgres-exporter. Two optional components are off by default: blockscout, a block explorer for EVM networks, and investor-portal, the Investor portal.
A separate support chart provides shared dependencies you can also choose to bring from elsewhere. By default it deploys PostgreSQL, Redis, RustFS (S3-compatible object storage), an ingress-nginx controller, and the reloader operator; an Envoy gateway and the Velero server are opt-in. The dalp chart owns what Velero backs up: its backup block renders the storage location and schedule.
The two charts are separate Helm releases installed side by side, never one nested inside the other. What each ships, and what it ships switched on:
dalp umbrella chart | On by default |
|---|---|
dapi, the Platform API | Yes |
dapp, the Console | Yes |
docs | Yes |
pgbouncer | Yes |
postgres-exporter | Yes |
blockscout | No |
investor-portal | No |
support chart | On by default |
|---|---|
| PostgreSQL | Yes |
| Redis | Yes |
| RustFS | Yes |
| ingress-nginx | Yes |
| reloader | Yes |
| Envoy gateway | No |
| Velero | No |
The support chart is optional in full: a deployment that already runs its own database, cache, object storage, and ingress installs dalp alone and points it at what it has.
Every dalp service reads its configuration from values.yaml, which embeds the same shape as the root config.yml and is layered by per-cluster overlays. Confirm the rendered ConfigMaps and Secrets contain the values your deployment requires.
The three Platform API roles
Each role is its own Deployment with its own autoscaler and disruption budget. The chart's defaults:
| Role | What it does | Scaling defaults | Availability defaults |
|---|---|---|---|
api | Serves Platform API and Console requests | 2 to 3 replicas at 80% average CPU; scale-down waits 300 seconds | A PodDisruptionBudget keeps 1 available; soft anti-affinity spreads pods across nodes |
runner | Executes the durable workflow engine and owns the workflow shard groups | 2 to 3 replicas at 75% average CPU; scale-in moves one pod at a time | A PodDisruptionBudget keeps 1 available; never selected by the HTTP Service |
indexer | Owns chain ingestion for the configured networks | 2 to 10 replicas at 65% average CPU; scale steps are one pod per window | A PodDisruptionBudget keeps 1 available; rollouts replace one pod at a time with no surge, and a replacement must stay Ready 90 seconds before the wave moves on |
The indexer floor of two is deliberate. A single indexer stops all chain ingest for its whole boot window on every restart, and a disruption budget of one against one replica allows zero voluntary disruptions, so a node drain is refused outright. Above the floor, replicas never add throughput for a single chain: live ingest keeps exactly one owner pod per chain, so extra replicas buy live owners for other chains, failover headroom, and capacity for fleet-wide backfill claims.
Every Platform API pod is stateless. Persistence lives entirely in PostgreSQL, Redis, and object storage, not in the pod. The Console (dapp) is stateless too, autoscaling 2 to 10 replicas on CPU.
Durable workflow execution shards work across every runner replica. The shard count (durable.sharding.shardsPerGroup, default 64) is a modulus every runner and client hashes entities through, which makes it cluster-wide configuration: two participants that disagree about it address the same work to different shards. Change it everywhere at once, as one coordinated configuration change, never per pod.
How queries reach PostgreSQL
Application queries reach PostgreSQL through PgBouncer, not directly. The umbrella chart enables the pgbouncer subchart by default and renders every service's database URL at the PgBouncer Service, so routing is the chart's decision end to end; no service opts in or out on its own. The database migration Job keeps a direct connection, as do the PostgreSQL exporter and optional Blockscout workload, so its schema changes run outside the pooler's transaction-mode constraints.
PgBouncer runs in transaction pooling mode, and the chart rejects every other mode. Two PgBouncer replicas each run one process, and every limit is per process: a process accepts up to 1000 client sockets by default while holding a small bounded pool of real server connections per user and database pair. The pooler is what lets replica counts scale without multiplying server connections: an extra api pod adds cheap client sockets, not PostgreSQL backends.
The budget that matters therefore lives in the pgbouncer chart's values. They price the aggregate peak, meaning every pooled alias across both peers, the reserve pools, and the direct consumers (the block explorer, the postgres-exporter, and the migration Job), against the PostgreSQL server's max_connections, 200 in the support chart's default. What goes wrong when you raise a pool size, a reserve, or the PgBouncer replica count without repricing: every per-process limit multiplies by the peer count, and the sum spends headroom the whole estate shares. Under saturation, clients queue instead of opening new server connections, and a client that queues past the configured wait timeout (20 seconds by default, with a saturation alert ahead of it) is rejected. The aggregate is watched through pg_stat_activity, not enforced per pod, so recheck the pricing whenever you change a pool value, a replica count, or max_connections.
Stateful dependencies
If you use the support chart rather than externally managed services, its defaults are development-shaped and need review before production:
| Dependency | Default topology | Note |
|---|---|---|
| PostgreSQL | A single Deployment (not a replicated cluster), 8Gi PVC | One shared instance backs multiple logical databases (the default database, blockscout, dapp), not one database per service |
| Redis | A single-replica Deployment | The chart's own value comment states it should be 1 for development |
| RustFS (object storage) | A single Deployment in standalone mode, 1Gi PVC | Object storage also supports AWS, GCP, Azure, a generic S3-compatible endpoint, and a filesystem path; only the active provider's configuration is used |
Cache and notification transport recovery
The cache is an optional runtime tier, even though Prerequisites requires Redis for the default multi-replica deployment, and the dalp chart pins the Redis cache backend on. A pod does not wait for Redis before it starts serving, because gating startup on it turns a brief interruption into a restart loop across every role. A pod that starts before Redis is reachable serves from its in-process tier and reconnects on its own.
Each pod supervises its cache and notification connections and retries on a bounded backoff with no limit on the number of attempts. Reconnection also checks that Redis is serving requests, not only accepting connections, so an endpoint that is loading a snapshot or failing over is retried rather than treated as healthy.
While a pod is disconnected, it cannot receive shared cache updates or invalidation notifications. Change notifications also stop, and each listener falls back to its own fixed reconcile bound instead: a lost cache-invalidation hint heals within cache.versioned.windowMaxLifetimeMs, 600,000 milliseconds (ten minutes) by default, and chain-health and network-configuration reloads heal within a fixed sixty seconds. These intervals bound scheduled reconciliation, not event delivery. Dependency outages and processing delays can extend the time until fresh state is visible. Operations that promise deployment-wide reach, such as clearing a cache namespace, fail while Redis is unreachable instead of reporting a success they cannot deliver.
dalp_redis_client_unavailable_seconds is zero while a pod's Redis connection is usable. During an outage, it reports how long the connection has been unavailable. The RedisClientLatchedOffline alert fires for a sustained outage, which separates a brief Redis restart from a pod that is not recovering.
Chart bundles by environment
global.deploymentEnvironment sets the value you configure: a closed set of deployment-environment names DALP recognizes, local, development, test, staging, demo, production. The dapi subchart's ConfigMap synthesizes the schema's platform.environment field from that value at render time, so platform.environment itself is not a key you set; global.deploymentEnvironment is.
Three chart shapes install a dalp release:
dalp-localbundles thedalp,support, and observability charts with an embedded chain, for thelocalenvironment, where nothing external is required.dalp-stagingis the same bundle shape pointed at a real cluster instead: its own umbrella chart pinning publisheddalp,support, and observability releases, configured by a cluster-specific values overlay. Use it for any pre-production environment reachable from outside your machine.productionhas no dedicated bundle chart. Install thedalpchart with your values overlay and managed PostgreSQL, Redis, and object storage. Keepsupportfor any components you still need in the cluster, such as the ingress controller. Its bundled datastores need production-specific configuration; see Stateful dependencies.
On OpenShift, in any of these, layer values-openshift.yaml, a security-context overlay for the same dalp chart templates, and the support and observability charts' own values-openshift.yaml over their releases; see OpenShift.
A further chart, dalp-migrate, is a values-only overlay with no templates of its own. It bundles the chain, support, and dalp dependencies together for a dedicated migration release line and pins workload names so upgrades keep the same persistent volume claims.
Deployment identity
Every deployment carries two identity values: global.dalpInstance names the instance, and global.deploymentEnvironment names its environment from the set above. Both default to local, and every non-local deployment must override them: they flow into telemetry labels and dashboards, so an instance that keeps the defaults cannot be told apart from any other in shared observability views. The dapi subchart, on by default in every dalp install, validates both: it requires each value present, checks global.dalpInstance against the 1 to 63 character lowercase slug rule, and checks global.deploymentEnvironment against the closed set above, failing the render before any pod starts. The observability chart adds one guard exclusive to it: a non-local environment that still carries the reserved local instance slug also fails the render. That chart is enabled by default in dalp-local; the dalp-staging bundle ships with it disabled, so the extra guard applies there only once an overlay turns observability on.
Consequences and reversibility
Scaling any role is a rollout, not a migration: replica changes move no data, because every Platform API pod is stateless, and scaling back requires another ownership and readiness transition. Raising pool ceilings without checking the connection budget can overload PostgreSQL, not just the role you scaled, so the pricing check has to happen alongside the replica change, not after it.
Keep at least two indexer replicas for restart headroom and voluntary node drains. Scaling changes chain ownership and readiness; verify indexing after the transition.
The shard count is the one value with no gradual path: every runner and client must agree on it, so it changes everywhere at once or not at all.
Each role's shutdown window is bounded by the chart's own numbers: a 50-second termination grace period, a 5-second preStop pause, and a 44-second DAPI_SHUTDOWN_BUDGET_MS that fits inside what remains. The budget covers the 20-second HTTP drain that DAPI_HTTP_DRAIN_TIMEOUT_MS bounds, the 15-second durable entity termination wait, and the 5-second telemetry flush. An api pod drains in-flight HTTP work within its drain timeout. A runner lets the cluster relocate its shards during the window. An indexer releases its chain ownership so a surviving replica can re-claim the chains it owned.
Frequently asked questions
Can the runner role pick up HTTP traffic if the api role is overloaded?
No. The runner's HTTP port is health-only: kubelet probes it, but the Service that routes application traffic never selects runner pods. Scaling the api role, not the runner role, is what adds HTTP capacity, and the two autoscalers stay independent so a workflow backlog on runner cannot starve the pool serving requests.
How much warning do I get before PgBouncer starts rejecting queued clients?
There is no guaranteed warning interval. PgBouncerClientMaxWait uses a one-second queue-wait threshold, a five-second hold, and a ten-second evaluation interval. Evaluation and notification delays can consume the remaining time before the 20-second query-wait timeout. Use the alert to investigate sustained pressure, not as a promise of time to prevent an individual rejection.
Why does PgBouncer scale by adding replicas instead of raising the number of processes in each pod?
Each PgBouncer pod runs one process by chart default, and that pin is deliberate. The pod's metrics exporter holds a single admin-console connection. A multi-process pod would route that connection to only one process at random, leaving the other processes' queue depth invisible to the saturation alert: clients could then hit the query wait timeout while monitoring reports no queue at all. Adding replicas instead keeps every process visible, because each replica carries its own exporter sidecar.
What happens if global.dalpInstance or global.deploymentEnvironment is left unset entirely, rather than kept at its default?
That fails differently than keeping the default. The dapi subchart's ConfigMap declares both values with Helm's required function, so an unset value fails that render immediately with a message naming the missing key. The observability chart, when enabled, repeats the same required guards, though there the environment check sits behind several unrelated Alloy credential checks, so a misconfigured Alloy receiver can fail that render first. Keeping the default local value, by contrast, satisfies every presence check and is caught only once the environment is anything but local, by the guard that rejects the reserved local slug outside a local environment.
Can I point a deployment at externally managed PostgreSQL, Redis, and object storage instead of the support chart?
Yes. The support chart is a separate, optional release: dalp-local, dalp-staging, and dalp-migrate all gate it behind support.enabled alongside the dalp chart itself, and the umbrella chart's services read their connection details from values.yaml regardless of who runs the backing service. Swapping the source means repricing the PgBouncer aggregate against the external server's own max_connections, not the support chart's default of 200.
Overview
Plan a topology and posture, prepare prerequisites, install the charts, integrate SSO and observability, and run the instance with upgrades and runbooks.
Hosting and access
Where a DALP Instance runs and how involved the vendor stays once it is running are separate decisions, chosen at install and revisited independently after.