Inside a Kubernetes Dashboard: Architecture Lessons from Volcano Dashboard

Volcano is a batch-scheduling system for Kubernetes. It handles high-throughput workloads like machine learning training, big data pipelines, and scientific simulations that the default Kubernetes scheduler was never designed for. Volcano Dashboard is the web UI for that system. It lets you monitor and manage Volcano resources directly inside your cluster.
The engineering choices underneath the UI are what make this project worth studying: two containers in one pod, clean proxying, a tight RBAC surface, and a backend with a clear scope. This article walks through those decisions and extracts patterns you can apply to your own Kubernetes-native dashboards.
What the Dashboard Actually Does
The dashboard gives you four views:
- Dashboard overview: stat cards and charts showing aggregate job, queue, and pod status across the cluster
- Jobs management: a searchable, filterable table of Volcano batch jobs (batch.volcano.sh/v1alpha1), with namespace, queue, and status filters
- Queues management: paginated Volcano scheduling queues (scheduling.volcano.sh/v1beta1) with state filtering
- Pods management: standard Kubernetes pods, sorted by creation time descending, filterable by namespace, name, and phase
One module is missing today: a Scheduler view.
Scheduler module implementation is planned soon. Expect a focused page for scheduler health, scheduling throughput signals, and configuration visibility.
Each view exposes a YAML dialog. Click any resource and inspect its raw manifest without leaving the browser.
The Two-Container Pod
The deployment runs both the frontend and backend in a single Kubernetes pod. This is the core structural decision.
Pod: volcano-dashboard
├── Container: frontend (Nginx, port 8080)
└── Container: backend (Express, port 3001)The frontend container serves the compiled React app and proxies every /api/* request to localhost:3001. Both containers share the pod’s network namespace, so “localhost” is literal. No DNS lookup, no network hop.
The React app calls /api/jobs. The request lands on the backend in dev and in prod. The frontend never stores a backend URL. This eliminates a category of environment-specific configuration bugs.
The Kubernetes Service exposes both ports:
| Service Port | Target Container Port | Purpose |
|--------------|------------------------|-------------------|
| 80 | 8080 (frontend/Nginx) | Browser traffic |
| 3001 | 3001 (backend/Express) | Direct API access |Pros of the single-pod model:
- Zero latency for API proxying (localhost)
- One deployment resource to manage
- No internal service-to-service routing needed
- Dev and prod share the same URL structure
Cons:
- Frontend and backend scale together. You cannot run three backend replicas against one frontend replica. Both grow in lockstep.
- A crash loop in one container affects the other’s restart policy.
- Pod eviction takes both containers down at once.
For a dashboard with read-heavy workloads and low concurrency, the trade-offs favor the simpler model. A high-traffic operational console would likely split these into separate deployments behind an ingress.
Nginx as the Proxy Layer
The Nginx configuration does one key thing:
location /api/ {
proxy_pass http://localhost:3001;
}All browser requests arrive at port 80. Requests to /api/ forward to the Express server on localhost:3001. Everything else, JS bundles, CSS, HTML, serves from Nginx’s static file root.
In local development, Vite replaces Nginx. The vite.config.js sets up an identical proxy rule: /api/* goes to http://localhost:3001. Your frontend code never knows whether it is talking to Vite or Nginx. One URL structure runs across both runtimes.
The Backend: Express Talking to Kubernetes
The Express server (backend/src/server.js) is the only part of this system that holds cluster credentials. The frontend carries no credentials at all. For any browser-facing application, this is the correct security posture.
Client Initialization
On startup, the backend loads Kubernetes credentials via KubeConfig.loadFromDefault(). Local development reads ~/.kube/config. In-cluster runs use the mounted service account token.
The server uses two API clients:
- CustomObjectsApi: for Volcano CRDs such as jobs, queues, and podgroups
- CoreV1Api: for Kubernetes pods and namespaces
Startup Verification
The server verifies CRD access before finishing startup:
await customObjectsApi.listClusterCustomObject('batch.volcano.sh', 'v1alpha1', 'jobs');
console.log('Volcano CRDs accessible');If this call fails, the server logs a clear error immediately. Without it, the server starts fine and every subsequent frontend API call returns a silent 500. With it, the failure scope narrows to RBAC, CRD installation, or API server connectivity.
Add a startup CRD probe to any backend that talks to Kubernetes custom resources. The cost is one API call at boot time.
API Design Patterns Worth Copying
Pattern 1: Keep routes aligned with resource types
Routes match your UI views, and they map cleanly to Kubernetes APIs.
- /api/jobs maps to batch.volcano.sh/v1alpha1 jobs
- /api/queues maps to scheduling.volcano.sh/v1beta1 queues
- /api/pods maps to core v1 pods
- /api/namespaces maps to core v1 namespaces
Pattern 2: Use a consistent list envelope
List endpoints return items plus totalCount. Paginated lists add page, limit, and totalPages. The frontend renders the response without computing counts or slicing arrays.
Pattern 3: Separate table data from dashboard aggregates
Tables need paging. Dashboard charts need full totals. Volcano Dashboard uses paginated endpoints for tables and /api/all-* endpoints for charts.
Pattern 4: Filter and sort on the server
Jobs support namespace, search, queue, and status filters. Queues support search and state filters plus pagination. Pods support namespace, search, and phase filters and sort by creationTimestamp descending.
Pattern 5: Add YAML views and guard write paths
Each key resource exposes a /yaml endpoint so you inspect raw manifests inside the UI. Create and patch endpoints validate manifests before calling the Kubernetes API.
Pods Behave Differently
The pods endpoint sorts by creationTimestamp descending before returning results. Newest pods appear first.
When something goes wrong with a batch job, you want to see the most recently created pods. Those are the ones most likely failing right now. Sorting alphabetically or by UID buries the relevant pods further down the list.
Deployment and RBAC: Permissions Match Features
The RBAC configuration reflects the feature list directly.
rules:
- apiGroups: ["batch.volcano.sh"]
resources: ["jobs"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: ["scheduling.volcano.sh"]
resources: ["queues"]
verbs: ["get", "list", "watch", "create", "delete", "update", "patch"]
- apiGroups: ["scheduling.volcano.sh"]
resources: ["podgroups"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods", "namespaces"]
verbs: ["get", "list", "watch"]Queues get update and patch because the UI has an edit form. Jobs get create and delete because the UI has those actions. Pods and namespaces are read-only because the UI only displays them.
Start from your UI feature list. Map each action to a Kubernetes verb. Grant exactly those verbs. Audit the RBAC every time you add a new feature. Granting broad permissions early and tightening them later rarely happens in practice.
Security Context
The deployment sets runAsNonRoot: true and drops Linux capabilities. Nginx needs writable paths for its cache and runtime files. The manifest mounts emptyDir volumes at /var/cache/nginx and /run. Both containers run unprivileged without breaking Nginx’s write requirements.
The Monorepo Structure
The project uses npm workspaces. A root package.json manages both the frontend/ and backend/ directories. Running npm run dev from the root uses concurrently to start Vite (frontend, port 3000) and Nodemon (backend, port 3001) at the same time.
Tech stack by layer:
Frontend: React 19, React Router 7, Material UI 6, Chart.js 4, Axios, Monaco Editor, Vite, Vitest
Backend: Express, @kubernetes/client-node, js-yaml, Babel, Nodemon in dev
Infrastructure: Docker with separate Dockerfiles per container, Kubernetes Deployment, Nginx
The frontend uses Vitest and React Testing Library for tests. Husky and lint-staged run pre-commit hooks to enforce formatting.
A Build Checklist for Your Own Kubernetes Dashboard
- Keep UI API calls relative under /api. Never hardcode a backend host in the frontend.
- Put a reverse proxy in front of the static frontend. Route /api/* to the backend.
- Return { items, totalCount } from every list endpoint. Add page, limit, and totalPages where lists grow.
- Mirror UI filters as query params. Apply them server-side. Return pre-filtered results.
- Expose /yaml endpoints for key resources. Use sortKeys: true or equivalent for stable output.
- Validate every write before touching the Kubernetes API. Reject wrong kinds, missing fields, and type mismatches.
- Run a startup probe for required CRDs and RBAC access. Log a clear status on both success and failure.
- Start RBAC from your feature list. Add write verbs only where the UI triggers writes.
- Set runAsNonRoot and drop capabilities. Mount emptyDir for any writable paths your server process needs.
- Use your build tool’s proxy in development to mirror the production reverse proxy config.
The full source is at github.com/volcano-sh/dashboard. The architecture documentation is at deepwiki.com/volcano-sh/dashboard.