GovernanceMode v3 · deployment

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.

host 84.8.125.207 k3s v1.36.2+k3s1 helm v3.21.2 ingress-nginx 4.15.1 namespace governance-mode release gm images 3.0.0-20261101

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.

Browser → 84.8.125.207:80

k3s ServiceLB opens port 80 on the node and forwards it to ingress-nginx.

all paths
ingress-nginx (ns ingress-nginx)

One Ingress rule, / → Service web. Large header buffers (160k) for the JWTs, 100 MB uploads.

/governance_mode/*
web (nginx + built SPA)

Serves the SPA and the uploaded files (each service's storage folder mounted read-only), and proxies /governance-mode/gateway/api/* to the gateway.

/governance-mode/gateway/api/* → :8000
api-gateway (Go)

Checks the JWT and API token, then calls the service over gRPC by its Service name.

gRPC
12 PHP services (RoadRunner gRPC)
user :50051notification :50053regulator :50054 governance :50055operations :50056workflow :50058 dashboard :50059messaging :50060task :50061 import-export :50062reporting :50063risk :50070

continuity (:50057) is switched off: it can't start until its repo commits config/database.php.

DB + cache
mongodb (StatefulSet, mongo 7.0.45) · redis 7.4.7

Reachable only from pods in the namespace (NetworkPolicy). Mongo holds the 16 databases copied from compose.

Browser → 84.8.125.207:7002 (websocket)

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.

PodImageRAM nowRAM limit
notificationgovernance-mode/notification321 Mi1 Gi
import-exportgovernance-mode/import-export295 Mi1.5 Gi
reportinggovernance-mode/reporting287 Mi1.5 Gi
usergovernance-mode/user285 Mi1 Gi
regulatorgovernance-mode/regulator285 Mi1 Gi
riskgovernance-mode/risk270 Mi1 Gi
messaginggovernance-mode/messaging254 Mi1 Gi
taskgovernance-mode/task253 Mi1 Gi
workflowgovernance-mode/workflow252 Mi1 Gi
operationsgovernance-mode/operations207 Mi1 Gi
governancegovernance-mode/governance207 Mi1 Gi
dashboardgovernance-mode/dashboard203 Mi1 Gi
mongodb-0mongo:7.0.45130 Mi2 Gi
notification-reverbgovernance-mode/notification61 Mi256 Mi
api-gatewaygovernance-mode/api-gateway15 Mi256 Mi
webgovernance-mode/web8 Mi256 Mi
redisredis:7.4.7-alpine4 Mi256 Mi
App total (17 pods)3.26 GiB16 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 limits add up to more than the VM has

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:

What compose is for now

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's storage/app
  • Copied once from the compose volumes during the switch. The marker file .migrated-from-compose stops 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 .env from ~/datamode/<repo>/.env
  • gm-shared: the JWT, gateway and service keys
  • gm-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

Known limits