Skip to content

Deploy Your Own Instance

This guide walks through standing up a complete Cloud-Native DCS instance and loading the fermentation reference plant: a bulk drug substance fermentation on Site plant-01, with a production fermenter fed by a seed train and a downstream recovery cell (five simulated units across two areas). At the end you'll have a working HMI, an ISA-88 physical hierarchy, and running batches against master recipes, all without any field hardware.

This is the "blessed starter" path. If you want to wire real field I/O over Modbus/EtherNet IP/OPC UA, finish this page first, then add controllers and IOModules per Device Enrollment.

If you want a worked production-grade install instead (Terraform-managed DigitalOcean cluster, Auth0 OIDC, Flux GitOps), start with cndcs-deploy-demo (scripts/bootstrap.sh runs the end-to-end provision). The requirements that worked example is satisfying live in Production Deployment, and Platform Administration § Requirements vs example maps each requirement to the specific path in that repo.

What You'll Need

Compute. Any x86_64 Kubernetes 1.27+ cluster will work. Tested configurations:

  • Talos Linux on a single x86_64 VM or industrial PC — what the reference deployments run
  • k3d or kind on an x86_64 Linux workstation — fastest for local exploration
  • Any managed cluster (GKE / EKS / AKS / DigitalOcean) with at least 2 CPUs and 4 GB RAM of headroom

x86_64 only

Release images are published for linux/amd64 only. ARM hardware (Raspberry Pi and other SBCs, Graviton instances) is a community/experimental tier. The published images will not pull there, so you would have to build your own. The configuration is untested.

Workstation tooling: kubectl, helm, git. No field hardware or wiring is required for the fermentation example. The simulation driver runs inside the unit runtime pods.

Registry access. Cloud-Native DCS is proprietary. The chart and every component image are published to a private GitHub Container Registry namespace, so you need a GitHub account granted read access to the cloud-native-dcs packages before any command on this page will work. Access is arranged per customer. Start at cndcs.io. Once you have it, create a GitHub personal access token with the read:packages scope. Section 2 uses it twice: once to pull the chart to your workstation, and once as a Kubernetes pull secret so the cluster can pull the images.

1. Provision a cluster

Pick one path. The rest of the doc is agnostic.

Talos Linux is what the reference deployments run. Boot an x86_64 machine (VM or industrial PC) from the Talos ISO, then from your workstation:

cat > single-node.yaml <<'EOF'
cluster:
  allowSchedulingOnControlPlanes: true
EOF
talosctl gen config dcs https://<node-ip>:6443 \
  --config-patch @single-node.yaml
talosctl apply-config --insecure -n <node-ip> --file controlplane.yaml
talosctl --talosconfig talosconfig config endpoint <node-ip>
talosctl --talosconfig talosconfig config node <node-ip>
talosctl --talosconfig talosconfig bootstrap
talosctl --talosconfig talosconfig kubeconfig

The config patch lets workloads schedule on the single control-plane node. For a worked, Terraform-managed version of this path, see cndcs-deploy-demo.

Other x86_64 distributions (kubeadm, k3s, k0s) work the same way. The rest of this guide only needs a kubeconfig.

k3d cluster create dcs --agents 1
# or: kind create cluster --name dcs

Any cluster with a functioning LoadBalancer or NodePort story works. Configure kubectl so the context points at the target cluster.

2. Install the DCS Helm chart

The chart is published to GitHub Container Registry as an OCI artifact. CRDs are installed by the chart, so you don't need a separate make install step.

Sections 1 through 3, run end to end against the published chart. Real wall time is five to eight minutes on a warm host, and the on-camera cuts elide only the waits.

Prerequisite: registry credentials. The registry namespace is private, so both your workstation and the cluster need the token from What You'll Need. Log helm in, and create the pull secret the pods will use:

export GHCR_USER=<your-github-username>
export GHCR_TOKEN=<your-read:packages-token>

# Your workstation, so `helm install` can pull the chart.
echo "$GHCR_TOKEN" | helm registry login ghcr.io -u "$GHCR_USER" --password-stdin

# The cluster, so the pods can pull the six component images.
kubectl create namespace dcs-system
kubectl create secret docker-registry ghcr-pull-secret \
  --namespace dcs-system \
  --docker-server=ghcr.io \
  --docker-username="$GHCR_USER" \
  --docker-password="$GHCR_TOKEN"

Verify the chart resolves before going further. This is the first command that touches the private registry. A failure here points at the token, since no cluster is involved yet:

helm show chart oci://ghcr.io/cloud-native-dcs/charts/cloud-native-dcs

A 401 unauthorized means helm is not logged in or the token lacks read:packages. Note that helm caches registry credentials, so a stale login on your workstation can make this appear to work while a colleague on a clean machine fails.

Prerequisite: cert-manager. The chart enables mTLS by default and creates a cluster-scoped CA issuer so per-site runtime Certificates can be issued by the physical-operator's Site reconciler. Install cert-manager first, and configure its controller to look for cluster CA Secrets in the DCS release namespace. The wait on the last line matters: the DCS install creates Certificate resources, and those go through cert-manager's admission webhook, so installing the DCS chart before the webhook is ready fails partway through the apply. (helm install --wait looks equivalent, but helm 4.2's status waiter can hang on this chart with every resource green. The explicit wait is the dependable form.)

helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --set installCRDs=true \
  --set clusterResourceNamespace=dcs-system
kubectl -n cert-manager wait --for=condition=Available deployment --all --timeout=180s

If you can't set clusterResourceNamespace, either disable the cluster-scoped mode (--set mtls.certManager.clusterIssuer=false --set mtls.certManager.perSiteRuntimeCert=false on the DCS chart, which loses per-site cert rotation) or move the chart's CA Secret into cert-manager namespace post-install.

Prerequisite: CloudNativePG, only if you enable the historian. The historian ships disabled (historian.enabled: false), so the walkthrough below does not need it. If you turn it on with the bundled database (historian.database.cnpg.enabled: true, the default), you need CloudNativePG chart 0.27.0 (operator 1.28) or newer. That is the first release whose CRD declares .spec.podSecurityContext, which the chart sets on the historian Cluster:

helm repo add cnpg https://cloudnative-pg.github.io/charts
helm install cnpg cnpg/cloudnative-pg \
  --namespace cnpg-system --create-namespace \
  --version '>=0.27.0'

Pin the floor at 0.27.0. A looser range happens to resolve new enough today and guarantees nothing. The DCS chart refuses to install against an older CNPG, so the mistake surfaces before the apply starts.

helm install dcs oci://ghcr.io/cloud-native-dcs/charts/cloud-native-dcs \
  --namespace dcs-system \
  --set 'global.imagePullSecrets[0].name=ghcr-pull-secret' \
  --set gateway.service.type=NodePort \
  --set gateway.auth.mode=none

gateway.auth.mode=none disables authentication — development only

This walkthrough builds a sandbox with no identity provider, so it runs the gateway with authentication off. Every visitor holds full administrative control. Never expose a cluster installed this way beyond your own machine, and never carry mode=none into an installation that touches real equipment. The chart's secure default is oidc, which requires an identity provider: the chart refuses to render oidc mode until gateway.auth.oidc.issuerURL names one. When you graduate to a real deployment, start from Production Deployment. Its hardening checklist covers OIDC/LDAP configuration.

global.imagePullSecrets is attached to every workload the chart creates. Without it the chart installs cleanly and every pod lands in ImagePullBackOff, so the install appearing to succeed is not the same as the release coming up.

Pods created later by the operators (unit-runtime, io-probe) live in site-<name> namespaces the Site reconciler provisions. They need the secret there too. Label it once and the reconciler copies it into every site namespace it creates and attaches it to that namespace's default ServiceAccount:

kubectl -n dcs-system label secret ghcr-pull-secret dcs.io/replicate-to-sites=true

Do this before creating the Site in section 3. Site Secret replication is on by default (siteSecretReplication.enabled: true). If you turned it off, copy the pull secret into each site namespace yourself.

Component image tags default to the chart's own appVersion, so every install runs the release the chart was published with. Don't override global.image.tag with a floating tag like latest.

The earliest installable chart version is 0.2.3. Versions 0.1.4 through 0.2.0 were withdrawn from the registry: each resolved a gateway image tag that an image rename left unpublished, so none of them could install (issue #1148). The release pipeline now verifies that every image a chart resolves exists in the registry before the chart is published.

Wait for the operator and gateway pods to report Running:

kubectl get pods -n dcs-system -w

Find the gateway's NodePort and open http://<node-ip>:<nodeport> in a browser (the gateway front port is plain HTTP, and TLS terminates at your ingress if you configure one). You'll land on an empty HMI, with no sites yet.

The chart also serves this documentation in-cluster, which matters on an air-gapped plant network. Once you front the gateway with an ingress, the docs appear under its /docs path, and docs.ingress gives them a hostname of their own.

See the Helm chart README for the full list of chart values (ingress, TLS, MQTT persistence, resource limits).

3. Apply the fermentation example hierarchy

The examples/fermentation/ set is a complete two-area ISA-88 hierarchy (Enterprise bioworks → Site plant-01 → upstream fermentation cell + downstream recovery cell) with recipes, phase templates, procedural elements, and alarms. The whole plant is declarative YAML, which is the point of the clip below. Clone the repo on your workstation:

git clone https://github.com/cloud-native-dcs/cloud-native-dcs.git
cd cloud-native-dcs
The plant you are about to apply is plain text: one phase template read and edited in the product's own editor, in VS Code, and in vim under git, with the edit that lands going through the governed change-control path.

Apply the shared control-module templates and the cluster-scoped resources first, wait for the site namespace, then apply the rest in order. 05b-controller.yaml (real-device enrollment) and 10-batch.yaml are deliberately skipped: a Batch triggers immediate execution, so you will create one from the UI in the next step instead. The 08a08c procedural templates go ahead of 08-recipes.yaml, which references them.

kubectl apply -f examples/templates/        # shared CM templates (vfd, valve, sensor)
kubectl apply -f examples/fermentation/01-enterprise.yaml
kubectl apply -f examples/fermentation/02-site.yaml
kubectl wait --for=condition=Ready \
  site/plant-01 --timeout=60s
for f in examples/fermentation/{03,04,05,05a,06,07,08a,08b,08c,08,09,11,12}-*.yaml; do
  kubectl apply -f "$f"
done

The physical operator creates unit runtime pods (one per unit). Each pod runs a simulated IOModule, with no field bus required. Verify:

kubectl get pods -n site-plant-01
kubectl logs -n site-plant-01 -l dcs.io/unit=fermenter-1 --tail=20

4. Drive it from the HMI

Refresh the HMI. The sidebar now shows bioworksplant-01upstreamfermentation-cell → three units (fermenter-1, seed-tank-1, media-prep-1), with the downstream recovery cell (centrifuge-1, chromatography-1) alongside. Click any unit to open its detail view with the control module faceplates.

From here:

  • Explore process values — click a temperature element (temp-element on the fermenter, or the seed and media vessels' RTDs) to see live scaled PVs driven by the simulation.
  • Actuate a valve — click a valve faceplate (media-valve, harvest-valve) and toggle CMD. The simulated feedback flips within one scan cycle.
  • Run a batchbds-v2 (a four-phase charge → inoculate → ferment → harvest fermentation on fermenter-1) arrives in Draft, like any recipe a plant has just authored. Open it under Recipes → Master Recipes and walk it through its lifecycle (Approve, Release, Activate), each step recording a signed, reasoned action. Once it is Effective, create a batch per Batch Execution. bds-v3 stays in Draft for walking the edit/promote lifecycle.
  • Watch alarmsexamples/fermentation/11-alarms.yaml registers AlarmDefinitions on fermenter-1 covering vessel temperature high/low and the agitator VFD fault interlock. Edit an IOModule's spec.simulation to inject a fault and watch the corresponding alarm fire and hold the running batch.
Beyond the HMI, the engineering side of the same install: the /system sidebar groups Diagnostics, Recipes → Master Recipes, Equipment Library → Control Modules, and Compliance → Change Requests.

5. (Optional) Add real field I/O

Once the fermentation plant is running end-to-end, wire real hardware by adding:

  1. A physical controller node — see Device Enrollment.
  2. An IOModule CR with a non-simulation driver — modbus or opcua for real field I/O (ethernetip exists but is experimental and gated behind --enable-experimental-drivers). See I/O for driver status and the authoring workflow.
  3. One or more ControlModule CRs with tagBindings pointing at the real I/O addresses. Use the canonical templates in Control Modules.

6. (Optional) Send data to a plant historian

Everything above stays inside the cluster. A plant whose system of record is its own historian takes the MQTT feed, and Historians and Data Archives covers what each product's usual connector expects.

An AVEVA PI System is the one destination with a shipped path. omfEgress in the chart turns on a component that posts OMF to the site's PI Web API endpoint and creates the AF elements and PI Points itself. It is off by default, it needs a credential and an egress rule before it will render, and the names it creates become a contract the moment a site publishes. Read OMF Egress to AVEVA PI before enabling it.

Troubleshooting

Runtime pod in CrashLoopBackOff. Check kubectl logs for ControlModule reference errors. A common cause is applying 07-controlmodules.yaml before the ControlModuleTemplates in examples/templates/ have been applied.

Pod stuck in Pending. The physical operator couldn't find a node matching the pod's node affinity. The fermentation units schedule onto the virtual node their simulation Controller enrolls (fermentation-controller upstream, recovery-controller downstream). Confirm the Controller from 05-controller.yaml was applied and its node exists, then check cluster capacity (kubectl describe pod -n site-plant-01 <pod>).

HMI loads but sidebar is empty. Cluster-scoped Enterprise and Site must exist before the namespaced resources in examples/fermentation/. Apply 01-enterprise.yaml and 02-site.yaml first and wait for the Site to report Ready.

For anything else, see Troubleshooting.

  • Production Deployment — storage class, topology, monitoring, hardening, and backup decisions for a customer-grade install. Read this before sizing a real cluster. The single-node install above takes shortcuts (single node, local non-replicated storage, no backups) that production must not repeat
  • cndcs-deploy-demo: the worked production-grade example (Terraform on DigitalOcean, Auth0 OIDC, Flux GitOps). See terraform/, flux/clusters/demo/, scripts/bootstrap.sh
  • Reference Architectures — what real plant topologies look like beyond a single cluster
  • Device Enrollment — join and register more controller devices
  • Security Hardening — production-readiness checklist (OIDC, TLS, network policies, electronic signatures)
  • Backup and Recovery — protect CRD state and audit records, including the gateway.archiveIntegrity scheduler that verifies every archive manifest on a cadence and the optional historian.audit.archival.immutable S3 Object-Lock mirror for 21 CFR Part 11 §11.10(c) restore from WORM
  • Historian Disk-Pressure Runbook — size the TimescaleDB PVC correctly and handle node disk pressure. If you're running a single-node dev/demo, also enable historian.prune.enabled: true in values.yaml so the hypertables are bounded in size. The prune CronJob ships with tolerateDiskPressure: true and priorityClassName: system-cluster-critical so it can still run when the node is shedding workloads under DiskPressure (issue #256).