How to provide the Doris telemetry store that the Helm chart points at with doris.host — choosing where Doris runs, the requirements, why in-cluster Doris uses the operator, installing the operator, applying the DorisCluster, and bringing Orbtrace up against it.
On Kubernetes the Helm chart does not deploy Doris — doris.mode defaults to external. You provide Doris and point the chart at it with doris.host. This page covers every supported way to do that, end to end.
Do this before `helm install`
The Orbtrace pod applies its schema to Doris before it binds the web port, so it stays un-Ready until Doris is reachable. Stand Doris up and confirm it is healthy first, then install Orbtrace.
Choose where Doris runs
Your setup
How to run Doris
Operator?
Evaluation / demo / single host
Use Docker Compose instead of Helm. The Compose bundle includes a Doris FE + BE — one command, nothing to set up separately.
No
Doris on a VM or a managed service
Run Doris off-cluster, reachable from the cluster, and set doris.host to its FE.
No
Doris inside the cluster (Kubernetes)
Run the doris-operator and apply a DorisCluster (below). The chart connects to the operator-managed FE Service.
Yes
Why the in-cluster path needs an operator
Doris is a clustered database — its frontend (FE) and backend (BE) nodes find each other by network address and have to stay registered as one group. On Kubernetes a pod gets a new IP every time it restarts, so the nodes would lose track of each other. Doris also needs certain steps done in a careful order: joining or removing a node, and upgrading the cluster one node at a time without downtime. Kubernetes' built-in pieces (a StatefulSet running the stock Doris image) don't know how to do any of that. The doris-operator is a small controller you install once that handles it for you — it joins new FE/BE nodes, keeps the membership correct when pods restart, and sequences safe rolling upgrades. On a VM or managed Doris the addresses never change, so there is nothing to coordinate and no operator is needed.
Requirements
For any in-cluster Doris (the operator path):
A Kubernetes cluster and Helm 3.8+.
A fast StorageClass — SSD/NVMe-backed. Doris BE compaction is IOPS-bound; spinning disks will not keep up.
A few Linux kernel settings on every node a Doris BE runs on. These live on the node (the host/VM), not inside the container — a normal pod can't change them, which is why you set them on the node up front. Elasticsearch and OpenSearch need the very same ones:
vm.max_map_count >= 2000000 — the maximum number of memory-mapped regions a process may hold. Doris BE memory-maps many data files; the Linux default (65530) is far too low and the BE refuses to start.
fs.inotify.max_user_instances = 8192 and fs.inotify.max_user_watches = 1048576 — how many files the node may watch for changes at once. Doris opens a lot of files; the defaults run out.
swap off — the BE runs swapoff -a at startup and expects real RAM, not swap. Size the node's RAM to the workload.
How to apply them depends on where the nodes are — concrete recipes (VM sysctl, a plain-Kubernetes DaemonSet, or the OpenShift Node Tuning Operator) are in Applying the node settings just below.
Cluster-admin rights — once — to install the operator. The doris-operator is cluster-scoped: it adds new resource types (CRDs) and cluster-wide permissions (RBAC), which only a cluster-admin may create. So an admin installs it one time for the whole cluster. After that, regular teams create a DorisCluster in their own namespace with ordinary (namespaced) permissions — no admin rights needed again.
For off-cluster Doris (VM/managed) you only need the node kernel settings above on the Doris hosts; there is no operator and no cluster-admin step.
Applying the node settings
Pick the recipe that matches where Doris BE runs.
On a VM / bare-metal host
Run on every Doris BE host, as root — a sysctl drop-in that also survives reboots:
Applysave as doris-node-sysctl.yaml, then runkubectl apply -f doris-node-sysctl.yaml
(Kubernetes nodes already run with swap off, so there is nothing extra to do there.)
On OpenShift
Don't use a privileged DaemonSet; apply the sysctls cluster-wide with the Node Tuning Operator (a Tuned custom resource) so the BE pods stay unprivileged under restricted-v2.
First check the operator is on your cluster — the Tuned resource type only exists when it is (real OpenShift ships it by default; OpenShift Local/CRC and some minimal OKD builds don't). If this returns NotFound, skip the YAML and use the fallback in the tip below:
Applysave as doris-node-tuning.yaml, then runoc apply -f doris-node-tuning.yaml
No operator on this cluster? Set the sysctls on the nodes directly
Without the operator the cluster does not know the Tuned resource type, so the oc apply above fails with no matches for kind "Tuned" in version "tuned.openshift.io/v1" / ensure CRDs are installed first. That error means exactly this case — write the same sysctls directly on each node (machine, not pod) Doris BE will run on instead. They hold only until the node reboots, and nodes added later need the same command. Fine for an evaluation:
# List the candidate nodes (the NAME column; OpenShift Local/CRC has one),# then apply on each — or on all workers in one shot with the loop:oc get nodes -l node-role.kubernetes.io/workerfor n in $(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[*].metadata.name}'); do oc debug node/$n -- chroot /host sysctl -w \ vm.max_map_count=2000000 \ fs.inotify.max_user_instances=8192 \ fs.inotify.max_user_watches=1048576done# Verify: oc debug node/<node-name> -- chroot /host sysctl vm.max_map_count
Scope the match to the nodes Doris BE lands on if you label a storage pool for it. Everything else OpenShift-specific (the chart overlay, SCC, Route) is on the OpenShift page.
Replication factor (RF) — match it to your BE count
The replication factor (RF) is how many copies of each row Doris keeps — one copy per backend (BE) node. It's what protects your data when a BE dies: with RF 2, every row lives on two BEs, so losing one is survivable. Orbtrace creates its Doris tables at replication_num = doris.profile.replicationNum (2 on the medium / large profiles — two copies).
The rule follows from that: RF = N needs at least N live BE nodes — you can't keep 2 copies of a row with only 1 node to put them on. So if you have fewer BEs than the RF, the very first CREATE TABLE fails (Doris returns replication num … available backend num is 1), Orbtrace stops on purpose during startup (a safety check in the migration runner), and its pod never turns Ready — it hangs un-Ready / crashloops. This is one of the most common "installed it but it won't come up" causes.
Two sides must agree — the number of BE nodes (Doris side) and the RF the app requests (Orbtrace chart values):
Side
Value
What it is
Doris
BE node count — beSpec.replicas (operator) or the number of BE hosts (VM)
the ceiling on RF
Orbtrace
doris.profile.replicationNum
the RF the app creates tables at — must be ≤ your BE count
Orbtrace
doris.allowSingleReplica
set true only to accept RF = 1 on a single BE
So the app-side parameter that changes with your Doris RF is doris.profile.replicationNum (plus doris.allowSingleReplica for a single BE). It is applied when the tables are created (first boot) — raising it later only affects new tables; existing tables keep their RF until you change it on the Doris side (ALTER TABLE … SET ("replication_num" = "N")). Decide your RF before first boot.
Production: run ≥ 2 BE with replicationNum ≥ 2 (3 on ≥ 3 BE for hyperscale). The reference DorisCluster below uses beSpec.replicas: 3, which satisfies the default RF = 2.
Single-BE (single-node / PoC): a single-BE Doris cannot satisfy RF = 2. To run against it, set both values in your my-values.yaml — accept the risk and actually create RF = 1 tables:
my-values.yaml
doris: allowSingleReplica: true # accept the data-loss risk profile: replicationNum: 1 # create tables at RF=1
RF = 1 means a single BE loss is permanent, unrecoverable data loss — never use it in production.
Why both keys, not just `replicationNum: 1`?
They do different jobs — one is the value, the other is a consent gate:
doris.profile.replicationNum is the RF the app writes into CREATE TABLE. Leave it at the default 2 and Doris itself rejects the create on a single BE (replication num should be less than the number of available backends).
doris.allowSingleReplica is a durability guard in the app (DorisMigrationRunner): it refuses to run below RF 2 unless you explicitly opt in — a fail-fast, exactly like doris.bundledAck, so nobody silently ships fragile RF=1 tables.
replicationNum
allowSingleReplica
Result
2 (default)
false
✅ normal (needs ≥ 2 BE)
1
false
❌ app aborts at boot — "below the durability floor … allow-single-replica is false"
2
true
❌ RF=2 CREATE TABLE still fails on one BE
1
true
✅ runs, with a loud data-loss warning
Only replicationNum: 1 → the guard stops boot. Only allowSingleReplica: true → the RF=2 create fails. Both together = it runs.
Option A — Doris on a VM or managed service (no operator)
Stand up a Doris FE + BE cluster on VMs (or use a managed Doris) on the same network as your cluster, with the node kernel settings applied on every BE host. Then point the chart at its FE — these keys go in your my-values.yaml (see Install with Helm):
my-values.yaml
doris: mode: external # default — the chart never runs Doris host: <your-doris-fe-host> # FQDN of your Doris FE, reachable from the cluster queryPort: 9030 # default Doris MySQL query port httpPort: 8030 # default Doris HTTP (Stream Load) port password: <your-doris-root-password> # empty ("") for a fresh Doris with no password
Add these to your my-values.yaml — the small overrides file you create in Install with Helm (step 3); step 4 there runs the install.
Sizing (FE/BE counts, CPU/RAM/disk) is in Capacity planning. That's all — skip the operator section below and continue at the Bring Orbtrace up section.
Option B — Doris inside the cluster with the operator
What you install
The doris-operator (SelectDB) — the controller that manages Doris on Kubernetes. Installed once per cluster.
A DorisCluster resource — your Doris instance (FE + BE), which the operator reconciles into running pods using the stock apache/doris FE/BE images.
Step 1 — Install the operator (one-time, cluster-admin)
On OpenShift: one-time grants in the Doris namespace
Two OpenShift security defaults block the operator's pods. The operator injects a privileged default-init container into its BE pods (it raises vm.max_map_count) — the default namespace Pod Security Admission (restricted/baseline) rejects any privileged container, so the BE pod is never created and the cluster stays initializing. And the stock apache/doris images run as root: under restricted-v2's arbitrary UID the FE crashloops with start_fe.sh: Permission denied — the anyuid SCC lets the pods keep the image's own user. Create the doris namespace (Step 2 deploys into it), relax its PSA, and grant both SCCs — the block is idempotent, safe to re-run:
(Running the label/policy commands without the namespace fails with namespaces "doris" not found — the create line above is why it's part of the block. Step 2's kubectl create namespace doris will then say AlreadyExists; that's fine.)
Skip it and the operator log reads would violate PodSecurity "…": privileged (container "default-init" must not set securityContext.privileged=true), the DorisCluster never leaves initializing, and the Orbtrace app crashloops on the missing BE. vm.max_map_count must still be >= 2000000 — the privileged init sets it, or apply it node-wide via the Node Tuning Operator (Requirements); either way these two grants are required. To avoid the privileged init and both grants entirely, run Doris off-cluster (Option A) — the recommended OpenShift path. Step-by-step on the OpenShift page.
Step 2 — Apply the DorisCluster
This DorisCluster is testbed-verified (a clean 60-minute sustained write on a 3-FE / 3-BE topology). The BE ConfigMap carries the compaction tuning that keeps writes from being rejected under sustained load — apply it together with the cluster. Set storageClassName to your fast SSD/NVMe class and scale BE replicas/memory per your Capacity planning.
kubectl create namespace doriskubectl apply -f doriscluster-orbtrace.yamlkubectl -n doris get doriscluster -w # wait for the FE/BE phase to go Ready
Evaluating on a small cluster? The reference CR won't schedule
The reference DorisCluster above is production-sized: 3 FE × 4 CPU + 3 BE × 8 CPU ≈ 36 CPU of requests. On a small cluster (OpenShift Local, single node, kind) its pods sit Pending with Insufficient cpu forever. For an evaluation only, replace the DorisCluster document in the file (keep the ConfigMap) with this 1 FE / 1 BE variant:
A single BE can only hold one copy of each row, so the chart must create tables at RF = 1 — set bothdoris.allowSingleReplica: true and doris.profile.replicationNum: 1 in your my-values.yaml (the Replication factor section explains why both). Never use this variant in production.
Don't override the FE config
Do not add an fe.conf via feSpec.configMapInfo — the operator's configMapInforeplaces the operator-injected fe.conf (priority networks, edit-log port) and the FE crashloops. The BE be.conf above is the safe path. For the one enterprise FE knob, set it at runtime once the cluster is up:
kubectl -n doris exec orbtrace-doris-fe-0 -- mysql -h127.0.0.1 -P9030 -uroot \ -e "ADMIN SET FRONTEND CONFIG ('max_dynamic_partition_num'='2000')"
Verify before you install Orbtrace
Confirm the cluster is healthy first — it saves you debugging an un-Ready Orbtrace pod later. For the operator path:
kubectl -n doris exec -it orbtrace-doris-fe-0 -- mysql -uroot -P9030 -h127.0.0.1 \ -e "SHOW FRONTENDS\G SHOW BACKENDS\G"
Every FE should be Alive: true and at quorum; every BE should be Alive: true. (For a VM/managed Doris, run the same query against its FE.)
Bring Orbtrace up
Point the chart at the Doris you just verified. The chart defaults already assume an operator Doris named orbtrace-doris in namespace doris:
my-values.yaml
doris: mode: external host: orbtrace-doris-fe-service.doris.svc.cluster.local # or your VM/managed FE queryPort: 9030 httpPort: 8030 password: "" # match the Doris root password (empty = fresh Doris)
Add these to your my-values.yaml — the small overrides file you create in Install with Helm (step 3); step 4 there runs the install.
These doris.* keys go into your my-values.yaml (the file you edit in Install with Helm, step 3). Two fields carry the connection:
doris.host — the FE address. For the operator, the default already matches the reference DorisCluster (orbtrace-doris in namespace doris), so if you used those names you don't need to change it. For a VM/managed Doris, set your FE's FQDN. (queryPort 9030 = MySQL wire the app reads over; httpPort 8030 = Stream Load — the standard Doris ports, override only if yours differ.)
doris.password — the Doris root password. Leave empty for a fresh operator Doris; set it (or use --set-string doris.password=…) if your DorisCluster/VM has a root password, or the app hangs un-Ready on an auth failure.
Then install Orbtrace per Install with Helm. On first boot the app's migration runner creates the otel_* tables and the Orbtrace extension columns — watch for it to finish:
kubectl -n orbtrace logs -f deploy/orbtrace-app -c orbtrace# ready when you see: [doris-migration] complete
The Orbtrace pod turns Ready once the schema is applied and it can reach Doris.
Troubleshooting
FE not reaching quorum — an even FE count or cross-node scheduling. The reference DorisCluster uses 3 FE with anti-affinity.
BE Alive: false — almost always vm.max_map_count not set on the BE node (Requirements), or the BE can't reach the FE edit-log port.
Orbtrace stays un-Ready / reports STORAGE_UNAVAILABLE — doris.host or doris.password mismatch, or Doris wasn't Ready when you installed. Re-check against the verify step above.