Deploy On-Prem
This guide deploys Zaris on a self-managed / bare-metal Kubernetes cluster — kubeadm, k3s, RKE2, Rancher, or similar — where there is no cloud load balancer and no managed DNS or certificate service. On-prem means you provide those pieces yourself: MetalLB hands out LoadBalancer IPs from your LAN, and you use your own DNS plus cert-manager or a corporate CA for HTTPS.
This is an environment-specific walkthrough. For how the chart works — the StatefulSet model, the connection-string client, scaling, and every value — read the Kubernetes overview first. This page assumes those concepts and focuses on the on-prem specifics.
Prerequisites
- A working self-managed Kubernetes cluster (kubeadm, k3s, RKE2, Rancher, etc.) with
kubectlconfigured against it and Helm v3. - A default StorageClass is NOT required. Zaris nodes are in-memory — the data volume is an
emptyDir— so no PersistentVolume and no CSI driver are needed. Durability comes from the replication factor. - A range of spare LAN IPs you can dedicate to MetalLB (for example a handful of unused addresses in your worker subnet that your DHCP server will not hand out).
- DNS you control for the console hostname — an internal DNS server, or a
/etc/hostsentry on your workstation for a lab.
Deploy Zaris (private first)
Start private and unauthenticated — the safe default. Create a namespace and install the chart (the store is auto-attached on deploy):
kubectl create namespace zaris
helm install zaris oci://registry-1.docker.io/clustron/zaris -n zaris \
--set node.replicas=4 \
--set node.replicationFactor=2
The chart image is public — pulling it needs no Docker login. node.replicas must be a multiple of node.replicationFactor (RF 2 → 4, 6, …); partitions = replicas ÷ RF. There is one store per release, named by cluster.id (default zaris-k8s), which is both the cluster id and the store name your clients use.
Wait for the nodes to become Ready (a Ready pod already owns and serves its share of the data):
kubectl -n zaris rollout status statefulset/zaris
Open the console privately with a port-forward — this needs no security because only you, through your kubeconfig, can reach it:
kubectl -n zaris port-forward svc/zaris-manager-console 8080:7810
# → http://localhost:8080
This works with zero extra infrastructure — no LoadBalancer, no DNS, no certs — so it is the right way to validate the deployment before you wire up MetalLB and ingress below. A 503 for the first few seconds is normal; the console URL is readiness-gated and goes live once a store is attached and serving.
(Optional) Dedicated cache nodes
For production it is a common pattern to keep the cache tier on its own machines so cache pods get predictable resources and app pods never crowd them. There is no cloud node-pool API on-prem — you label and taint your chosen worker nodes directly.
Pick the machines that will host the cache tier and, on each, apply a label and a matching taint:
kubectl label node <node> workload=zaris
kubectl taint node <node> workload=zaris:NoSchedule
How the two settings work together:
- The taint
workload=zaris:NoSchedulerepels everything from those nodes — ordinary app pods (which carry no matching toleration) will never schedule there. - The label
workload=zarisis what Zaris'snodeSelectortargets — it keeps Zaris pinned to those nodes. - Because Zaris also carries a toleration for the taint, it is the one workload allowed onto them.
Pin the whole chart (nodes and manager) with a values file:
# cache-nodes.yaml — pin the chart to your labelled/tainted machines
node:
nodeSelector:
workload: zaris
tolerations:
- key: workload
operator: Equal
value: zaris
effect: NoSchedule
manager:
nodeSelector:
workload: zaris
tolerations:
- key: workload
operator: Equal
value: zaris
effect: NoSchedule
Add -f cache-nodes.yaml to the helm install/helm upgrade.
With RF 2, node.replicas should be a multiple of the replication factor so every partition has both copies on distinct pods. On three cache nodes you have three sensible choices:
- RF 3 (
--set node.replicationFactor=3 --set node.replicas=3) — every partition has a copy on all three nodes; survives 2 node losses. Simplest for exactly 3 nodes. - RF 2, replicas 3 — the chart warns that one partition ends up single-copy (3 ÷ 2 doesn't divide evenly); tolerable for non-critical data, but not fully durable.
- 4 cache nodes with RF 2, replicas 4 — clean 2× replication with room to lose any one node.
Give the cluster LoadBalancer IPs (MetalLB)
Bare-metal Kubernetes has no cloud load balancer, so a type: LoadBalancer Service stays stuck at EXTERNAL-IP: <pending> forever — nothing is there to assign an address. MetalLB fills that gap by handing out IPs from a pool of your LAN addresses.
Install MetalLB:
helm install metallb metallb \
--repo https://metallb.github.io/metallb \
-n metallb-system --create-namespace
Then tell it which addresses it may hand out. Create an IPAddressPool from your spare LAN range and an L2Advertisement so MetalLB answers ARP for those IPs on the local network:
kubectl apply -f - <<'EOF'
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: zaris-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.240-192.168.1.250 # a spare, unused range on your LAN
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: zaris-l2
namespace: metallb-system
spec:
ipAddressPools:
- zaris-pool
EOF
Use a range that is on your worker subnet but outside your DHCP scope, so nothing else claims those addresses. Now any type: LoadBalancer Service — the console below, or the ingress controller — gets a real IP from this pool.
Expose the console (LoadBalancer via MetalLB, HTTP)
With MetalLB in place, the console can take a LoadBalancer IP. External exposure always requires control-plane security — the chart refuses to render loadBalancer (or ingress) unless manager.security.enabled=true with an admin credential, so you can never accidentally put an unauthenticated admin console on the network.
helm upgrade zaris oci://registry-1.docker.io/clustron/zaris -n zaris --reuse-values \
--set manager.console.expose=loadBalancer \
--set manager.security.enabled=true \
--set manager.security.adminPassword='<change-me>' \
--set manager.console.loadBalancer.loadBalancerIP=192.168.1.240 \
--set 'manager.console.loadBalancer.sourceRanges={192.168.1.0/24}'
loadBalancerIPpins a specific address from the MetalLB pool (optional — omit it and MetalLB picks one).sourceRangesis a CIDR allowlist — restrict it to your LAN (or a tighter range) rather than leaving the console open.- The default
admin/admincredentials must not ship — change the password. In production prefer--set manager.security.existingSecret=<secret>(a Secret with keysadmin-username/admin-password) over an inline password. The manager self-provisions the admin before the pod is Ready, so the URL is never published un-provisioned.
Get the assigned IP:
kubectl -n zaris get svc zaris-manager-console
Browse to http://<metallb-ip> and sign in. Note the management port (7801) is never exposed externally — only the console (7810) is published.
A LoadBalancer is a layer-4 (TCP) load balancer — it does not terminate TLS, so the console is served over plain HTTP. Fine for a quick internal check; for anything real, use the ingress + HTTPS path below.
Console over HTTPS (ingress-nginx + cert-manager)
HTTPS needs an ingress (where TLS is terminated) and a DNS hostname (a certificate is issued for a name, not an IP). On-prem the ingress controller's own Service is also type: LoadBalancer, so it takes a MetalLB IP.
1. Install the ingress-nginx controller:
helm install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
-n ingress-nginx --create-namespace
2. Get the controller's MetalLB IP:
kubectl -n ingress-nginx get svc ingress-nginx-controller
3. Point your DNS at it. Map the console hostname to that IP — either an A record on your internal DNS server, or, for a lab, an /etc/hosts entry on your workstation:
zaris-console.corp.local → <ingress-metallb-ip>
Now choose a certificate. There are two options — the second is the usual on-prem path.
(a) cert-manager + Let's Encrypt
cert-manager can auto-issue a publicly-trusted Let's Encrypt cert — but only if the console host is publicly resolvable and reachable from the internet for the ACME HTTP-01 challenge. Most on-prem/internal hostnames (like zaris-console.corp.local) are not public, so this usually will not work for internal deployments. Use it only when your host is genuinely public; otherwise use option (b).
helm install cert-manager cert-manager \
--repo https://charts.jetstack.io \
-n cert-manager --create-namespace \
--set crds.enabled=true
kubectl apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: you@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
EOF
Then deploy the console as an ingress with --set manager.console.ingress.tls.clusterIssuer=letsencrypt-prod (see the AKS guide for the full flag set).
(b) Bring your own / corporate-CA cert (recommended for internal)
The common on-prem path: use a certificate issued by your corporate CA (or any internal PKI). Browsers trust it automatically as long as your corporate CA is already in their trust store — which, for domain-joined machines, it usually is.
Create a TLS Secret from your cert and key, then deploy the console as an ingress pointing at that Secret:
kubectl -n zaris create secret tls console-tls \
--cert=fullchain.pem --key=key.pem
helm upgrade zaris oci://registry-1.docker.io/clustron/zaris -n zaris --reuse-values \
--set manager.console.expose=ingress \
--set manager.console.ingress.className=nginx \
--set manager.console.ingress.host=zaris-console.corp.local \
--set manager.console.ingress.tls.enabled=true \
--set manager.console.ingress.tls.secretName=console-tls \
--set manager.security.enabled=true \
--set manager.security.adminPassword='<change-me>'
The result: https://zaris-console.corp.local with no browser warning on any machine that trusts your corporate CA. TLS terminates at the ingress using your cert — never Zaris's internal CA. For internal deployments, prefer this over Let's Encrypt.
Cluster TLS (node-to-node + client encryption)
The console TLS above secures the browser↔console hop. A separate, independent layer secures the data plane — node↔node replication and client↔node traffic — via node.tls.enabled with a cluster CA and one per-node leaf certificate mounted from a Secret.
Generating the CA and per-node leaves is covered in depth in the security docs — follow those to produce ca.pem and a Secret of <nodeId>.pfx leaves:
- CA and trust modes — the four trust models and which to pick.
- Certificate management — generating the CA + per-node multi-SAN leaves and creating the Secret.
Once you have the CA and the cert Secret, enable data-plane TLS:
helm upgrade zaris oci://registry-1.docker.io/clustron/zaris -n zaris --reuse-values \
--set node.tls.enabled=true \
--set node.tls.certSecret=zaris-node-certs \
--set-file node.tls.caPem=ca.pem
node.tls.certSecret is a Secret whose keys are per-node <nodeId>.pfx leaves; node.tls.caPem is supplied from a local file with --set-file. Clients then connect with the zariss:// scheme and the cluster CA:
zariss://<host>:7861/zaris-k8s?ca=/path/ca.pem
Connect your app
In-cluster applications connect through the client Service DNS. There is one store per release, named by cluster.id (default zaris-k8s):
# plaintext
zaris://zaris-client.zaris.svc.cluster.local:7861/zaris-k8s
# TLS (when cluster TLS is enabled)
zariss://zaris-client.zaris.svc.cluster.local:7861/zaris-k8s?ca=/path/ca.pem
Replace zaris in the DNS name with your namespace, and zaris-k8s with your cluster.id if you changed it. See the Kubernetes overview for the C# / ASP.NET and PowerShell client examples.
Scale
Scaling has two dimensions on-prem:
More capacity — add and prepare a worker node. Join it to the cluster the usual way for your distribution (kubeadm join, k3s/RKE2 agent, etc.), then, if you use dedicated cache nodes, label and taint it:
kubectl label node <new-node> workload=zaris
kubectl taint node <new-node> workload=zaris:NoSchedule
More Zaris pods (capacity + partitions) — scale the chart. Scaling out is lossless resharding — new pods join and the partition map grows onto them with no restart of existing pods:
helm upgrade zaris oci://registry-1.docker.io/clustron/zaris -n zaris --reuse-values \
--set node.replicas=6
Keep node.replicas a multiple of node.replicationFactor. Scale in one step at a time (it is not yet drain-safe) — see the scaling notes.
Air-gapped clusters
If the cluster cannot reach Docker Hub, mirror the artifacts into your internal registry first: pull-and-push the chart (oci://registry-1.docker.io/clustron/zaris:1.1.0) plus both images (clustron/zaris-node:1.1.0 and clustron/zaris-manager:1.1.0), then point the chart at your registry:
helm install zaris oci://<registry>/clustron/zaris -n zaris \
--set node.image.repository=<registry>/zaris-node \
--set manager.image.repository=<registry>/zaris-manager \
--set node.replicas=4 --set node.replicationFactor=2
Mirror the MetalLB, ingress-nginx, and cert-manager charts + images the same way if you use them.
Tear down
helm uninstall zaris -n zaris
kubectl delete namespace zaris # also removes the cert / CA Secrets
If you installed MetalLB, ingress-nginx, or cert-manager only for this deployment, remove them too:
helm uninstall metallb -n metallb-system
helm uninstall ingress-nginx -n ingress-nginx
helm uninstall cert-manager -n cert-manager
kubectl delete namespace metallb-system ingress-nginx cert-manager
Next steps
- Understand the trust model behind mounted certificates → CA and trust modes.
- Compare against the cloud walkthroughs → Deploy on AKS.
- Review the deployment models → Deployment overview.