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.
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
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 08a–08c 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 bioworks → plant-01 →
upstream → fermentation-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-elementon 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 toggleCMD. The simulated feedback flips within one scan cycle. - Run a batch —
bds-v2(a four-phase charge → inoculate → ferment → harvest fermentation onfermenter-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-v3stays in Draft for walking the edit/promote lifecycle. - Watch alarms —
examples/fermentation/11-alarms.yamlregisters AlarmDefinitions onfermenter-1covering vessel temperature high/low and the agitator VFD fault interlock. Edit an IOModule'sspec.simulationto inject a fault and watch the corresponding alarm fire and hold the running batch.
/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:
- A physical controller node — see Device Enrollment.
- An
IOModuleCR with a non-simulation driver —modbusoropcuafor real field I/O (ethernetipexists but is experimental and gated behind--enable-experimental-drivers). See I/O for driver status and the authoring workflow. - One or more
ControlModuleCRs withtagBindingspointing 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.
Related Documentation¶
- 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). Seeterraform/,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.archiveIntegrityscheduler that verifies every archive manifest on a cadence and the optionalhistorian.audit.archival.immutableS3 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: truein values.yaml so the hypertables are bounded in size. The prune CronJob ships withtolerateDiskPressure: trueandpriorityClassName: system-cluster-criticalso it can still run when the node is shedding workloads under DiskPressure (issue #256).