Flextenant Chart
The flextenant chart deploys the two core components of flex.plane: the orchestrator (GraphQL API and control plane) and the portal (web dashboard). Both run as separate deployments within the same Helm release.
Configure the orchestrator
The orchestrator is the stateless API gateway. It needs to know about your OIDC provider, mesh network, and tenant configuration.
# charts/flextenant/values.yaml
oidc:
clientId: "<portal-client-id>" # From Zitadel portal app
issuer: "https://id.flexplane.example.com"
zitadel:
orgId: "<zitadel-org-id>" # From Zitadel console
projectId: "<zitadel-project-id>" # From Zitadel console
privateKeySecret: "zitadel-key" # Secret created in identity setup
headscale:
domain: mesh.flexplane.example.com # Mesh control server domain
tenant:
name: "My Organization"
domain: "flexplane.example.com"
orchestrator:
replicaCount: 1
image:
repository: ghcr.io/zeitlos/flex.plane/orchestrator
tag: "a1b2c3d"
controlUrl: "https://mesh.flexplane.example.com"
resources:
requests:
cpu: 10m
memory: 192Mi
limits:
memory: 1Gi
The orchestrator exposes several ports:
| Port | Service |
|---|---|
| 8080 | GraphQL API and playground |
| 9090 | Headscale control server |
| 50443 | Headscale gRPC |
| 7777 | Terminal proxy (VM console WebSocket) |
Persistence
The orchestrator requires a persistent volume for Headscale state (SQLite database, node keys):
orchestrator:
persistence:
enabled: true
storageClass: "" # Uses cluster default
size: 10Gi
accessMode: ReadWriteOnce
Container registry
If your Proxmox nodes pull images from a private registry, configure the registry credentials:
containerRegistry:
host: "ghcr.io"
imagePullSecret: "regcred"
Configure the portal
The portal is a Nuxt 3 application that serves the web UI.
portal:
replicaCount: 1
image:
repository: ghcr.io/zeitlos/flex.plane/portal
tag: "a1b2c3d"
resources:
requests:
cpu: 10m
memory: 128Mi
limits:
memory: 1Gi
The portal reads its OIDC configuration from the orchestrator's settings. The key environment variables it needs are:
NUXT_OIDC_CLIENT_ID: same as the orchestrator's OIDC client IDNUXT_OIDC_ISSUER_WELL_KNOWN: the OIDC discovery URLNUXT_PUBLIC_BASE_URL: the public URL of the portalNUXT_PUBLIC_TENANT_NAME: the display name for your tenant
These are typically set via extraEnv or derived from the chart values.
Deploy the chart
kubectl create namespace flexplane
helm install flextenant charts/flextenant \
-n flexplane \
-f my-flextenant-values.yaml
Watch the rollout:
kubectl rollout status deployment/flextenant-orchestrator -n flexplane
kubectl rollout status deployment/flextenant-portal -n flexplane
Configure ingress
Both the orchestrator and portal support Kubernetes ingress. Enable it in the values:
orchestrator:
ingress:
enabled: true
tlsEnabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt"
portal:
ingress:
enabled: true
tlsEnabled: true
className: "nginx"
path: "/portal"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt"
Verify the ingress resources were created:
kubectl get ingress -n flexplane
If you are using cert-manager for TLS, check that certificates are issued:
kubectl get certificates -n flexplane
Ingress paths
The default routing is:
| Path | Service | Purpose |
|---|---|---|
/graphql | Orchestrator | GraphQL API endpoint |
/playground | Orchestrator | GraphQL playground (development) |
/portal | Portal | Web dashboard |
/portal path by default. All API routes (/graphql, /playground, /health/*) are handled by the orchestrator. Make sure your ingress routes these paths to the correct service. In production, disable the GraphQL playground by setting orchestrator.extraEnv to include PLAYGROUND=false.Health checks
The orchestrator exposes health endpoints for Kubernetes probes:
| Endpoint | Purpose |
|---|---|
/health/live | Liveness probe: is the process alive? |
/health/ready | Readiness probe: is it ready to serve traffic? |
These are configured automatically in the chart's pod spec. The startup probe allows up to 5 minutes for initial startup (useful when Headscale is initializing).
Verify the deployment
Check the orchestrator health
curl -s https://flexplane.example.com/health/ready
A healthy orchestrator responds with a 200 OK.
Open the portal
Navigate to https://flexplane.example.com/portal/ in your browser. You should see the flex.plane login screen. Click Login to be redirected to Zitadel for authentication.
After logging in, you will see the dashboard. It will be empty since no nodes or VMs exist yet. That is expected.
Test the GraphQL API
If the playground is enabled, open https://flexplane.example.com/playground and run a test query:
query {
zones {
id
name
}
}
This will return an empty list until you register your first node, but it confirms the API is working and authentication is configured correctly.
Authorization header in the playground to Bearer <your-token>. You can grab a token from the portal's browser session (check the network tab in dev tools).