GovernanceMode on k3s
Since 2026-11-01 15:44 UTC the whole GovernanceMode v3 stack runs on a single-node k3s cluster on 84.8.125.207, deployed with one Helm chart. The Docker Compose stack it replaced is stopped and kept only for rollback.
How a request reaches a service
The app is served on the bare IP over plain HTTP at http://84.8.125.207/governance_mode/ (no domain yet). Everything enters through one Ingress; the app's own nginx inside the web pod does the routing, the same file the compose stack used.
k3s ServiceLB opens port 80 on the node and forwards it to ingress-nginx.
One Ingress rule, / → Service web. Large header buffers (160k) for the JWTs, 100 MB uploads.
Serves the SPA and the uploaded files (each service's storage folder mounted read-only), and proxies /governance-mode/gateway/api/* to the gateway.
Checks the JWT and API token, then calls the service over gRPC by its Service name.
continuity (:50057) is switched off: it can't start until its repo commits config/database.php.
Reachable only from pods in the namespace (NetworkPolicy). Mongo holds the 16 databases copied from compose.
Live notifications bypass the ingress: Service notification-reverb is a LoadBalancer, so ServiceLB opens 7002 directly. The SPA has this port built in.
What runs, and what it uses
Measured with kubectl top about 10 minutes after the switch, at idle. Every pod has a health check, and Kubernetes restarts a pod that fails it.
| Pod | Image | RAM now | RAM limit |
|---|---|---|---|
| notification | governance-mode/notification | 321 Mi | 1 Gi |
| import-export | governance-mode/import-export | 295 Mi | 1.5 Gi |
| reporting | governance-mode/reporting | 287 Mi | 1.5 Gi |
| user | governance-mode/user | 285 Mi | 1 Gi |
| regulator | governance-mode/regulator | 285 Mi | 1 Gi |
| risk | governance-mode/risk | 270 Mi | 1 Gi |
| messaging | governance-mode/messaging | 254 Mi | 1 Gi |
| task | governance-mode/task | 253 Mi | 1 Gi |
| workflow | governance-mode/workflow | 252 Mi | 1 Gi |
| operations | governance-mode/operations | 207 Mi | 1 Gi |
| governance | governance-mode/governance | 207 Mi | 1 Gi |
| dashboard | governance-mode/dashboard | 203 Mi | 1 Gi |
| mongodb-0 | mongo:7.0.45 | 130 Mi | 2 Gi |
| notification-reverb | governance-mode/notification | 61 Mi | 256 Mi |
| api-gateway | governance-mode/api-gateway | 15 Mi | 256 Mi |
| web | governance-mode/web | 8 Mi | 256 Mi |
| redis | redis:7.4.7-alpine | 4 Mi | 256 Mi |
| App total (17 pods) | 3.26 GiB | 16 Gi | |
| k3s server + containerd | ~0.8 GiB | – | |
| ingress-nginx + kube-system | ~0.2 GiB | – | |
| Deployment total | ~4.3 GiB |
CPU
0.57 of 8 vCPU (7%) for the whole node at idle. Each PHP service is capped at 1 vCPU.
Disk
Root disk 68% (16 GB free of 48). The k3s image store takes 4.2 GB, and data is 0.44 GB under /srv/governance-mode. The old Docker images still hold 12.6 GB that could be freed.
Compared with compose
The app itself uses the same ~3.3 GB it did on compose. k3s adds about 1 GB of its own (API server, containerd, ingress controller).
The memory limits total 16 Gi on a 12 GB VM, and the reservations are only ~2.4 Gi. That's fine at idle. If several heavy exports run at once, Kubernetes evicts pods once free memory drops below 800 Mi, and the gm-watchdog acts at 80% RAM.
Can compose and k3s run at the same time?
Not usefully, and it isn't recommended. Memory alone would almost fit, but two other things block it:
- Memory: at idle the two together reach ~78%, just under the 80% line where the gm-watchdog starts stopping things. One export or image build would cross it.
- Ports: both need port 80 and 7002 on the same IP. Compose would have to move to other ports. The built SPA calls
http://84.8.125.207and:7002, so a compose front end on other ports would still talk to the k3s backend unless its web image is rebuilt. - Data: each stack has its own copy of Mongo and uploads. Running both means two copies drifting apart.
Rollback only. Its containers are stopped but not removed, and its volumes were never changed. They don't restart after a reboot.
Data and secrets
Data on the node
/srv/governance-mode/mongodb, Mongo's data files (hostPath)/srv/governance-mode/<service>, each service'sstorage/app- Copied once from the compose volumes during the switch. The marker file
.migrated-from-composestops the copy from ever running again over newer data.
Secrets
- No secret is in Helm values or on a command line.
gm-<service>: that service's whole.envfrom~/datamode/<repo>/.envgm-shared: the JWT, gateway and service keysgm-registry: the image pull login- Pods restart by themselves when a Secret changes.
Images come from registry.servers.advancedcontrols.tech/governance-mode/*:3.0.0-20261101. Every tag is pinned, the chart refuses to render without one, and the push script refuses to overwrite an existing tag.
Day-to-day operations
The kit lives in ~/gm-k3s on the host (chart, site values, scripts, pinned k3s/helm/ingress files). RUNBOOK.txt there has every step.
# state
kubectl -n governance-mode get pods
kubectl -n governance-mode top pods
# full smoke test through the public IP (asks for the Admin1 password;
# creates one test regulator and removes it again)
BASE=http://84.8.125.207 WS_HOST=84.8.125.207 bash ~/gm-k3s/scripts/30-validate.sh
# new build: rebuild gm3/*, bump global.image.tag in site/84.8.125.207.yaml, then
bash ~/gm-k3s/scripts/05-push-images.sh
sudo bash ~/gm-k3s/scripts/20-deploy.sh
# changed a .env? same deploy; the pods roll
sudo bash ~/gm-k3s/scripts/20-deploy.sh
Rollback to compose
sudo k3s kubectl -n governance-mode scale deploy,sts --all --replicas=0
sudo k3s kubectl -n ingress-nginx scale deploy --all --replicas=0
sudo k3s kubectl -n governance-mode delete svc notification-reverb # frees :7002
sudo k3s kubectl -n ingress-nginx delete svc ingress-nginx-controller # frees :80/:443
cd ~/datamode/docker-stack-v3 && docker compose up -d
Compose comes back with its data as of the switch (2026-11-01 15:42 UTC). Anything changed on k3s after that is not in it.
Guard rails
- gm-watchdog (systemd on the host): at 80% RAM, disk or sustained CPU it first stops GovernanceMode batch jobs (builds, pushes, imports, helm). Then it scales this namespace to 0, then stops compose. It never touches ITSM.
- kubelet eviction: pods are evicted before the node runs out of memory (<800 Mi) or disk (<10%). 1 GiB is reserved for the system.
- Health checks: each PHP service is checked on the RoadRunner status port (:2114) and by the image's own healthcheck script (scheduler and queue still running). The gateway is checked on
/up, Mongo with a ping, Redis with PING. - Never two copies: deployments use the Recreate strategy. A second copy of a service would share its files and its scheduler.
Known limits
- A single node, with no high availability. If the VM goes down, the app does too.
- Plain HTTP on the IP only. A domain needs a DNS record first. After that, the chart can get a Let's Encrypt certificate with two value changes (
global.domain,global.tls.mode), and the web image has to be rebuilt for the new URL. - Mongo runs without authentication, the same as compose and the reference host. Only the NetworkPolicy keeps it inside the namespace.
- Business Continuity (
continuity) is off until its repo is fixed. - Exports always come back as "queued" (HTTP 202). The service goes async on its own, and the file arrives as an in-app notification. The email copy fails because the admin's address isn't deliverable.