Skip to main content

Kubernetes Installation (Helm)

PlantPulse provides a Helm chart for running the platform on Kubernetes.

The full stack has not yet been verified

I have confirmed a clean install up to the data lake with this chart (2026-09-09, services 19/19, no privileges). However, server · batch · warehouse · proxy have not been tested with actual hardware yet.

For customer installations, use One-Line Installation or Docker Installation.

What It Is​

This chart translates the Docker Compose configuration into Kubernetes. It is not a replacement.

The source of truth remains the compose file. If the two diverge, the compose file is correct and the chart is wrong. When new settings appear, they are added to the compose file first, and the chart follows.

Chart Locationplantpulse-platform-k8s repository's charts/plantpulse-platform
Image Registrydocker.kopens.io/pp for the platform, docker.kopens.io/proxy-dockerhub for third-party
Test Environmentk3s v1.32.5+k3s1 · Single Node
Compose Services CoveredAll 10

Before You Begin​

Kubernetes Version​

For Oracle Linux 8.10, k3s must be the v1.32 line

That OS uses cgroup v1, and k3s v1.36 (Kubernetes 1.36) has removed cgroup v1 support. If you upgrade, kubelet will say this and won't start.

kubelet is configured to not run on a host using cgroup v1

Storage — RWX is Required​

Multiple pods attach simultaneously to the pp-security volume that holds the TLS materials. therefore, a provisioner that supports ReadWriteMany is required — such as NFS, CephFS, or Longhorn RWX.

The default k3s local-path only works with a single node

local-path is RWO and node-local. On a single node, all pods land on that node, so it works, but the moment a second node joins, it silently breaks. This is not a flaw in the chart but an actual constraint, and I note it here so it isn't discovered during a customer installation.

Resources​

Platform resource requirements don't shrink just because you're using Kubernetes. Follow System Requirements as-is — a minimum of 16 vCPU · 128GB RAM · 1TB SSD.


If You Don't Have a Cluster — K3s → Rancher → Helm​

If you're already using Kubernetes, skip this section and go to Installation. If you're starting from scratch, the shortest path is to create a cluster with K3s, add a UI with Rancher, and then deploy the PlantPulse chart on top.

Rancher is not our product

It's SUSE's open-source Kubernetes management UI. It's optional, and kubectl alone is sufficient for installation and operation. It's convenient when you want to see pods, logs, and events visually, so I've included it here.

1. Install K3s​

This is a single-node cluster. The installation script brings up the container runtime, kubelet, and default storage (local-path) all at once.

# Oracle Linux 8.10 은 cgroup v1 이라 v1.32 라인을 고정합니다 (위 경고 참고)
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.32.5+k3s1 sh -

sudo systemctl status k3s # active (running)
sudo k3s kubectl get nodes # STATUS = Ready

To use kubectl from a regular account, copy the kubeconfig.

mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown "$(id -u):$(id -g)" ~/.kube/config
kubectl get nodes
Open the firewall first

K3s uses 6443 (the API). If you plan to attach other nodes, you'll also need 8472/udp (flannel VXLAN) and 10250 (kubelet) between nodes. The ports the platform itself uses are in Port Configuration.

2. Install Rancher​

Rancher needs certificates, so cert-manager must be up first.

helm repo add rancher-stable https://releases.rancher.com/server-charts/stable
helm repo add jetstack https://charts.jetstack.io
helm repo update

# 1) cert-manager
helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set crds.enabled=true

kubectl -n cert-manager rollout status deploy/cert-manager

# 2) Rancher — <호스트> 는 브라우저로 칠 주소입니다
helm install rancher rancher-stable/rancher --namespace cattle-system --create-namespace --set hostname=<호스트> --set bootstrapPassword=<초기비밀번호> --set replicas=1

kubectl -n cattle-system rollout status deploy/rancher

Go to https://<호스트> in your browser, log in for the first time with the bootstrapPassword you created above, and change the password immediately. Once logged in, this K3s cluster will be registered as local.

If it's a private network, set the host name first

Rancher creates certificates based on hostname. If you plan to access it via IP, put that IP directly into hostname, and accept the browser's warning about the self-signed certificate.

3. Install PlantPulse​

From here on, it's the same whether you use kubectl or the Rancher UI. Follow the Installation section below exactly. If you're using Rancher, you can register the chart repository under Apps → Charts → Repositories and install from the UI, but the helm command is better for working with value files.

Where Rancher really helps is after installation — seeing in one screen where a pod is waiting in its Init stage, what the logs say, and what an event has rejected. That's especially true when reading Startup Order.


Installation​

1. Create Secrets​

The chart contains no passwords at all. They're not in values.yaml, and in the default configuration the chart doesn't create Secrets either — it only references existing ones.

The source of truth is the same sidecar file as in compose. Write it byte-for-byte.

kubectl -n plantpulse create secret generic plantpulse-platform-env \
--from-env-file=/etc/kopens/plantpulse-platform.env

2. Install​

helm install pp charts/plantpulse-platform -n plantpulse --create-namespace

The default registries (docker.kopens.io/pp · docker.kopens.io/pd) are public, so they can be pulled from outside. This is why --set isn't needed for a routine installation.

3. If You've Mirrored to a Private Registry​

helm install pp charts/plantpulse-platform -n plantpulse --create-namespace \
--set global.image.registry=<사설 레지스트리>/pp \
--set global.image.registryDatalake=<사설 레지스트리>/pd \
--set global.image.registryThirdParty=<사설 레지스트리>/proxy-dockerhub \
--set global.imagePullSecrets[0].name=<레지스트리 시크릿>
Don't forget registryThirdParty

If you cover just the first two, the busybox used for startup-order waiting can't be pulled. Then all dependent pods will sit in Init:0/1 forever, and that failure doesn't look like a registry problem.


Startup Order — Kubernetes Won't Enforce It​

The platform has a required order.

certs → datalake → server → batch
→ warehouse · opcua · aasx · ha
server → proxy

Kubernetes has no equivalent to the compose depends_on. The things people commonly say they use instead all do something different.

Common ApproachWhat It Actually Does
readinessProbeBlocks traffic to that pod. Can't delay another pod's startup
StatefulSetOrder of pods within the same StatefulSet. Unrelated to other workloads
Helm hook · --waitOrder of manifest application. Not the readiness of what's been created

So the chart places the waiting where Kubernetes actually enforces it — in initContainers. The wait checks not "has the pod come up" but "is the output present."

Init:0/2 is not a failure

It means the chart is working. Ask what it's waiting for.

kubectl -n plantpulse logs <파드> -c wait-for-certs

The default wait timeout is 1800 seconds, checked every 5 seconds. If it times out, the wait ends and kubelet restarts the pod — so the reason is left in the pod events. If it were blocked forever, nothing would be left.

The chart doesn't touch ordering inside containers. The startup order within the data lake is enforced by pd inside the container. If the chart re-implemented it, there would be two sources of truth that diverge.


If the Proxy Returns 502​

A Docker network forwards all container ports, but a Kubernetes Service only forwards the ports it declares. This is where an upstream that worked in compose becomes a 502 in Kubernetes.

The chart has a gate to catch this — it checks that every proxy upstream points to a Service created by this chart, and to a port that Service declares.

bash hack/run-gates.sh
GateWhat It Checks
gate-render.shWhether the proxy upstreams in the rendered result point to real Services and ports
gate-no-plaintext-secrets.shWhether the rendered result contains any plaintext secrets
prove-gates-fail.shWhether the gates actually fail when they should (the gate's gate)

Run it before installation and make it pass. Most 502s are caught here first.


How Big Should a Node Be​

🔴 The sum of the requests declared by the chart is exactly the node's requirement. If it's short, pods won't start and will stay in Pending — the first cause of "installation seems stuck."

WorkloadCPU RequestMemory RequestMemory Limit
datalake864 Gi64 Gi
server212 Gi16 Gi
batch16 Gi8 Gi
warehouse14 Gi8 Gi
opcua · aasx0.5 each2 Gi each4 Gi each
ha0.51 Gi4 Gi
proxy0.2256 Mi1 Gi
certs0.1256 Mi1 Gi
Total (default config)~14~92 Gi—
The data lake accounts for most of it

datalake alone requests 8 CPUs · 64 Gi of memory (request equals limit — it's pinned as Guaranteed). To run on a single node, that node needs to be comfortably in the CPU 16 · memory 128 Gi range.

JVM Heap Is Declared Separately from Pod Limits​

The chart passes the heap size directly to the Java app set. It's not derived from the pod limit — if it were, lowering the limit would silently shrink the heap, and in practice a literal in the image would win, leaving the JVM unaware regardless of how you adjust the limit.

AppHeapDirectVariables Passed
server8g4gPP_SERVER_HEAP · PP_SERVER_DIRECT
batch3g—PP_BATCH_HEAP
warehouse1g—PP_WAREHOUSE_HEAP

The chart asserts the following at render time. If violated, the installation fails on the spot.

힙 + 다이렉트 + 여유(기본 2048Mi) ≤ limits.memory
힙 + 여유 ≤ requests.memory

🔴 If you raise the heap without raising the pod limit, you'll get an OOMKill during pre-touch. -Xms=-Xmx is set to AlwaysPreTouch, so the heap is fully resident right at startup, and the logs won't point to the heap. Raise both together.


What Has Been Verified So Far​

The test environment is k3s v1.32.5+k3s1 · single node, using entirely actual registry images.

ComponentStatus
plantpulse-certs✅ Ready. TLS materials actually created on the shared volume
plantpulse-datalake✅ Clean install with this chart, 19/19 services up (2026-09-09, 2026.09 image, after PVC deletion)
Startup order waiting✅ Verified blocked and unblocked with actual hardware
server · batch · warehouse · proxy⬜ No record of having been brought up yet — though chart-side flaws are still being caught (2026-09-09: backup volume, JVM heap contract, proxy upstreams)
opcua · aasx · ha⬜ Verified only up to the waiting state
mirror-makerDefault off (corresponds to the compose mirror profile)

The data lake pod now comes up without privileges — with systemd removed from PID 1 in the image, privileged · SYS_ADMIN · cgroup:host are all no longer needed.

TOTAL SERVICES : 19 RUNNING / 0 STOPPED
/proc/1/comm : bash # systemd 아님
securityContext: (비어 있음) # privileged·SYS_ADMIN·cgroup 없음
Remaining gates

No one has yet gotten to the point of verifying the server → batch → warehouse → proxy order with actual hardware.

This isn't a claim that the chart is being left untouched. On 2026-09-09, I fixed a set of app-tier flaws — the backup volume not attaching to the server, the JVM heap being unavailable in Kubernetes, and the seven proxy upstreams still being in Docker's shape. But all three were found by render comparison and code review, not by actually bringing up the pods. "Fixed" and "runs with this chart" are different facts. For customer installations, use One-Line Installation or Docker Installation.


When You're Stuck​

SymptomWhat to Check First
Pod stuck in Init:0/1Might be unable to pull busybox — check registryThirdParty
Pod waiting in Init:0/2This is normal. See kubectl logs <파드> -c wait-for-certs for what it's waiting on
Proxy route returns 502bash hack/run-gates.sh — whether the upstream points to a real Service and port
TLS-related errors after adding a second nodepp-security isn't RWX
Warehouse console lists always emptyA required Iceberg setting is empty. Empty values are supposed to fail rendering, so check the installation logs