A Kubernetes-native platform for managing container-based workspaces and desktops via a web UI.
Modelled on the Kubeflow Notebooks architecture but as a standalone, lightweight solution.
| Component | Path | Description |
|---|---|---|
| Controller | controller/ |
Kubernetes controller (kubebuilder) that reconciles Workspace, User, and AuthConfig CRs |
| API | api/ |
REST API service (Goa framework) providing workspace CRUD, volumes, images, auth, and a reverse proxy for workspace web UIs |
| Frontend | frontend/ |
Next.js web UI with dashboard, workspace management, user management, namespace filtering, dark mode |
| Deploy | deploy/ |
Helm chart, Kustomize manifests, and ArgoCD Application for deployment |
- Full PodSpec flexibility per workspace (like Kubeflow Notebook CRD)
- Browser-based access to workspaces via built-in reverse proxy (WebSocket support)
- Optional authentication via OIDC (Dex, Okta, Auth0, or any OIDC provider)
- Kubernetes-native RBAC — three roles: admin, editor, viewer
- Personal namespaces — auto-created per user with configurable naming template
- No database required — all state in CRDs, Secrets, and native RBAC objects
- Namespace filtering with global selector persisted in localStorage
- Dark mode with class-based toggle
- Volume (PVC) management - create, list, attach to workspaces
- Start/Stop workspaces without deleting them (annotation-based)
- Admin section with user management, auth settings, API docs, and CRD browser
- Workspace detail view with Overview, Logs, Events, Metrics, and YAML tabs
- kubectl with access to a Kubernetes cluster
- kind (for a local cluster)
- Helm 3.8+ (only for the Helm install path)
Building the components from source additionally needs Go 1.24+ (controller) / 1.26+ (API and proxy), Node.js 20+ (frontend) and Docker. See each component repo for its own developer workflow — this repo only holds deployment manifests.
Deploy the published images to a throwaway kind cluster:
kind create cluster
# CRDs must use server-side apply (the Workspace CRD exceeds the
# client-side annotation size limit)
make install-crd
make deploy-kustomize
make port-forward-frontendOpen http://localhost:3000. Authentication is disabled by default, so no login is required — see Authentication to enable it.
To tear it down: kind delete cluster.
Before going to production, set your own hostnames — see
docs/domains.md for how to override the placeholder
domains via Helm values or kustomize patches.
kubectl apply -f argocd/application-crds.yaml
kubectl apply -f argocd/application.yamlApply the CRDs Application first — the components Application will not sync cleanly against missing CRDs. Note that Argo CD syncs from the git remote, so it deploys the last pushed commit rather than your local working tree.
helm install kube-workspaces helm/kube-workspaces/ \
--namespace kube-workspaces-system --create-namespaceOr straight from the published chart, without cloning this repo:
helm install kube-workspaces \
oci://ghcr.io/kube-workspaces/charts/kube-workspaces \
--namespace kube-workspaces-system --create-namespaceInstalling into a pre-existing namespace that is managed elsewhere (e.g. a shared namespace provisioned by another tool) requires disabling creation of the release namespace, since Helm cannot adopt a namespace it did not create:
helm install kube-workspaces oci://ghcr.io/kube-workspaces/charts/kube-workspaces \
--namespace my-shared-namespace \
--set namespaces.createReleaseNamespace=falseWithout this, the install fails with
invalid ownership metadata; label validation error: missing key "app.kubernetes.io/managed-by".
The workspace namespace is still created — control it with
namespaces.createWorkspaceNamespace.
kubectl apply --server-side -k kustomize/crds/
kubectl apply --server-side -k kustomize/base/Released images are published to GHCR and are what the manifests reference by default — you do not need to build anything to deploy:
| Component | Image |
|---|---|
| controller | ghcr.io/kube-workspaces/controller |
| api | ghcr.io/kube-workspaces/api |
| proxy | ghcr.io/kube-workspaces/proxy |
| frontend | ghcr.io/kube-workspaces/frontend |
To build from source, clone each component repo alongside this one and build
from its root (each repo has its own Dockerfile):
for c in controller api proxy frontend; do
docker build -t "kube-workspaces-$c:dev" "../$c"
doneFor kind clusters, load the locally built images and deploy with the test
overlay, which switches imagePullPolicy to IfNotPresent so the loaded
images are actually used:
kind load docker-image \
kube-workspaces-controller:dev kube-workspaces-api:dev \
kube-workspaces-proxy:dev kube-workspaces-frontend:dev
kubectl apply --server-side -k kustomize/overlays/test/Authentication is opt-in and disabled by default. When disabled, the system operates without login — all users have full access (preserving backward compatibility).
Authentication is opt-in. To enable it, create an AuthConfig CR and necessary secrets.
Note for Google OIDC: Ensure your Redirect URI is set to https://<YOUR-DOMAIN>/auth/callback in the Google Cloud Console.
apiVersion: kubeworkspaces.io/v1alpha1
kind: AuthConfig
metadata:
name: default
spec:
enabled: true
oidc:
issuerURL: https://accounts.google.com
clientID: <YOUR-GOOGLE-CLIENT-ID>
clientSecret:
name: kube-workspaces-oidc-secret
key: client-secret
session:
signingKey:
name: kube-workspaces-session-secret
key: signing-key
personalNamespaces:
enabled: true
template: "{{username}}"
registration:
autoProvision: true
defaultRole: editor
adminEmails:
- your-email@gmail.comCreate the required secrets:
kubectl create secret generic kube-workspaces-oidc-secret \
--from-literal=client-secret=YOUR_CLIENT_SECRET \
-n kube-workspaces-system
kubectl create secret generic kube-workspaces-session-secret \
--from-literal=signing-key=$(openssl rand -hex 32) \
-n kube-workspaces-system- Dex (recommended for multi-provider support) — supports LDAP, SAML, GitHub, GitLab, etc.
- Okta — direct OIDC integration
- Auth0 — direct OIDC integration
- Any OIDC-compliant provider
Users are managed as User CRDs (cluster-scoped). They can be managed via:
- The Admin UI at
/admin/users - kubectl:
kubectl get users.kubeworkspaces.io
Users are auto-provisioned on first OIDC login when registration.autoProvision is enabled.
apiVersion: kubeworkspaces.io/v1alpha1
kind: User
metadata:
name: jane-doe
spec:
email: jane@example.com
displayName: "Jane Doe"
role: editor
namespaceAccess:
- namespace: team-platform
role: editor| Role | Permissions |
|---|---|
| admin | Full access to all namespaces, user management, settings |
| editor | Create/edit/delete workspaces in assigned namespaces |
| viewer | Read-only access to assigned namespaces |
When enabled, each user gets a personal namespace automatically created by the User controller. The namespace name is derived from the configurable template (default: {{username}}).
The controller also creates:
- A
RoleBindinggranting the user editor access - An optional
ResourceQuota(if configured in AuthConfig)
The Workspace custom resource wraps a full Kubernetes PodSpec, giving complete flexibility over container configuration:
apiVersion: kubeworkspaces.io/v1alpha1
kind: Workspace
metadata:
name: my-workspace
namespace: workspaces
spec:
template:
spec:
containers:
- name: code-server
image: codercom/code-server:latest
args: ["--bind-addr", "0.0.0.0:8080", "--auth", "none"]
ports:
- containerPort: 8080
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"Workspaces are stopped by adding the annotation kubeworkspaces.io/stopped: "true", which sets the StatefulSet replicas to 0. Removing the annotation starts the workspace.
Cluster-scoped CRD defining available workspace images with default configuration:
apiVersion: kubeworkspaces.io/v1alpha1
kind: Image
metadata:
name: code-server
spec:
image: codercom/code-server:latest
displayName: "Code Server (VS Code)"
defaultPort: 8080
icon: vscodeThe full catalog of Image manifests lives in
kube-workspaces/image-catalog,
which is the source of truth. This repo vendors a pinned release of it into
images.yaml and helm/kube-workspaces/files/images-*.yaml —
see Image Catalog Sync in
CONTRIBUTING.md for how that vendoring works.
-
Kustomize:
make install-imagesapplies the full vendoredimages.yaml(currently 38 images).kustomize/basedoes not create anyImageCRs on its own — this is a required separate step. -
Helm: installs a curated set of 5 example images by default (
installExampleImages: true). SetinstallCatalogImages: trueto install the full catalog instead, orinstallExampleImages: falseto install neither. Add your own images via theimages:values list regardless of which catalog setting you use.# Full catalog via Helm helm install kube-workspaces helm/kube-workspaces/ \ --namespace kube-workspaces-system --create-namespace \ --set installCatalogImages=true
The API includes a built-in reverse proxy at /proxy/{namespace}/{name}/{path...} that provides direct browser access to running workspace web UIs.
- WebSocket support: Full WebSocket passthrough (needed for noVNC's
websockifyand code-server) - Location header rewriting: Redirects from workspace apps stay under the proxy prefix
- Escaped request handling: Requests that escape the proxy prefix (e.g., apps referencing
/sw.jsor absolute paths) are caught via theRefererheader and rerouted - No-op ServiceWorker: Apps that try to register a ServiceWorker at root scope get a no-op SW
- Per-image proxy configuration: Each image can declare proxy behavior hints
Port-forward to access the UI and workspace proxies:
make port-forward-frontend # localhost:3000 -> frontend UI (includes proxy)
make port-forward-api # localhost:8888 -> API (direct proxy access, better WebSocket)Connect to workspaces via the UI "Connect" button, or directly:
- Code Server:
http://localhost:8888/proxy/workspaces/{name}/ - Debian Desktop:
http://localhost:8888/proxy/workspaces/{name}/vnc.html?resize=remote
| Method | Path | Description |
|---|---|---|
| GET | /v1/workspaces |
List workspaces (supports ?namespace= filter) |
| GET | /v1/workspaces/{name} |
Get workspace |
| POST | /v1/workspaces |
Create workspace |
| PUT | /v1/workspaces/{name} |
Update workspace |
| DELETE | /v1/workspaces/{name} |
Delete workspace |
| POST | /v1/workspaces/{name}/start |
Start workspace |
| POST | /v1/workspaces/{name}/stop |
Stop workspace |
| GET | /v1/workspaces/{name}/logs |
Get container logs |
| GET | /v1/workspaces/{name}/events |
Get workspace events |
| GET | /v1/workspaces/{name}/pod |
Get pod details |
| GET | /v1/workspaces/{name}/metrics |
Get pod metrics |
| GET | /v1/volumes |
List volumes (supports ?namespace= filter) |
| POST | /v1/volumes |
Create volume |
| DELETE | /v1/volumes/{name} |
Delete volume |
| GET | /v1/images |
List available images |
| GET | /v1/namespaces |
List namespaces |
| GET | /healthz |
Health check |
| GET | /openapi3.json |
OpenAPI 3.0 spec (JSON) |
| GET | /proxy/{ns}/{name}/{path...} |
Reverse proxy to workspace web UI |
| GET | /auth/config |
Public auth configuration |
| GET | /auth/login |
Initiate OIDC login |
| GET | /auth/callback |
OIDC callback |
| POST | /auth/logout |
Clear session |
| GET | /auth/me |
Current user info |
| GET | /admin/users |
List users (admin) |
| POST | /admin/users |
Create user (admin) |
| PUT | /admin/users/{name} |
Update user (admin) |
| DELETE | /admin/users/{name} |
Delete user (admin) |
| GET | /admin/auth-config |
Get AuthConfig (admin) |
| PUT | /admin/auth-config |
Update AuthConfig (admin) |
| GET | /admin/crds/definitions |
List CRD definitions |
| GET | /admin/crds/workspaces |
List raw workspace CRs |
| Route | Description |
|---|---|
/ |
Dashboard with summary cards and workspace list |
/login |
SSO login page (shown when auth enabled) |
/workspaces |
Workspace table with status, actions |
/workspaces/new |
Create workspace form |
/workspaces/{name} |
Workspace detail (Overview, Logs, Events, Metrics, YAML) |
/volumes |
Volume list |
/volumes/new |
Create volume form |
/images |
Available images catalog |
/admin |
Admin index (visible to admins only when auth enabled) |
/admin/users |
User management (list, create, enable/disable, delete) |
/admin/settings |
Auth settings (OIDC config, namespaces, registration) |
/admin/api |
API documentation (Scalar) |
/admin/images |
Image CR editor |
/admin/crds |
CRD browser |
Prompts for driving an LLM coding agent (Claude Code, Codex, Cursor, …) through a deployment. Each one is self-contained, states explicit success criteria, and avoids blocking commands so the agent does not hang waiting on a foreground process.
Deploy kube-workspaces to my Kubernetes cluster using the current kubectl context. Do not create or switch clusters — confirm the context first with
kubectl config current-contextand stop and ask me if it is not what I expect.
- Clone
https://github.com/kube-workspaces/deploy.gitand work from the repo root.- Install the CRDs:
kubectl apply --server-side -k kustomize/crds/. Server-side apply is mandatory — the Workspace CRD embeds a full PodSpec and is ~658 KiB, far over the 256 KiBlast-applied-configurationannotation limit, so plainkubectl apply -ffails.- Install the components:
kubectl apply --server-side -k kustomize/base/. The manifests already point at the publishedghcr.io/kube-workspaces/*images, so do not build any images.- Install the workspace image catalog:
make install-images. This applies the cluster-scopedImageCRs fromimages.yaml. Skipping this leaves the UI catalog empty —kustomize/basedoes not create anyImageCRs.Then verify, and report a pass/fail line for each check:
- All six CRDs are Established:
kubectl wait --for=condition=Established crd/workspaces.kubeworkspaces.io crd/images.kubeworkspaces.io crd/users.kubeworkspaces.io crd/authconfigs.kubeworkspaces.io crd/platformconfigs.kubeworkspaces.io crd/poddefaults.kubeworkspaces.io --timeout=60s- All four deployments are Available:
kubectl wait --for=condition=Available deployment --all -n kube-workspaces-system --timeout=300s(expectkube-workspaces-controller,-api,-proxy,-frontend)- No container has restarted. Every pod must show
0restarts and noCrashLoopBackOff:kubectl get pods -n kube-workspaces-system -o wide- The API is healthy. Start a background port-forward, poll, then kill it:
kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-api 8888:80 &thencurl -fsS http://localhost:8888/healthzmust return{"status":"ok"}.curl -fsS http://localhost:8888/v1/imageslists the catalog entries you applied in step 4.- The frontend serves HTML: background-forward
svc/kube-workspaces-frontend 3000:80and checkcurl -fsS http://localhost:3000/returns HTTP 200 with an HTML body. The frontend has no/healthzendpoint —/is its probe path.If any deployment fails to become Available, diagnose before continuing:
kubectl describe podon the not-ready pod,kubectl logsfor its containers, andkubectl get events -n kube-workspaces-system --sort-by=.lastTimestamp. Report the root cause rather than retrying blindly.Finally, tell me the exact commands to re-open the port-forwards myself, and do not leave any background port-forward processes running.
Deploy kube-workspaces to a local kind cluster.
kind create cluster --name kube-workspaces- Clone
https://github.com/kube-workspaces/deploy.git, then from the repo root runmake install-crd && make deploy-kustomize && make install-images.- Wait for readiness:
kubectl wait --for=condition=Available deployment --all -n kube-workspaces-system --timeout=300sVerify with a background port-forward (never a foreground one):
kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-api 8888:80 &→curl -fsS http://localhost:8888/healthzreturns{"status":"ok"}kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-proxy 8891:80 &→curl -fsS http://localhost:8891/readyzreturns{"status":"ok"}kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-frontend 3000:80 &→curl -fsS http://localhost:3000/returns HTTP 200 and HTMLKill every port-forward you started when done. Note that the Ingress in
kustomize/basehardcodesingressClassName: traefikand a placeholder hostname, so it is inert on a default kind cluster — port-forwarding is the only way in. Do not try to make the Ingress work.Report each check as pass/fail, and finish with the single command I need to delete everything (
kind delete cluster --name kube-workspaces).
Run this after either deployment above to prove the controller and proxy actually work, not just that the pods started:
Using the current kubectl context with kube-workspaces already deployed, create a workspace and verify it end to end.
Apply this
WorkspaceCR. Note that a rawWorkspacedoes not need a matchingImageCR —ImageCRs only populate the UI/API catalog and supply defaults at creation time through the API. Usetraefik/whoamirather than a heavyweight IDE image so the pull is a few MB and the check is fast:apiVersion: kubeworkspaces.io/v1alpha1 kind: Workspace metadata: name: smoke-test namespace: workspaces spec: template: spec: containers: - name: whoami image: traefik/whoami ports: - containerPort: 80 name: workspace-portThe
workspacesnamespace already exists —kustomize/basecreates it.Assert the controller reconciled it. It creates a StatefulSet and a Service both named after the workspace, and the pod is
smoke-test-0:
kubectl rollout status statefulset/smoke-test -n workspaces --timeout=180s(prefer this overkubectl wait --for=jsonpath=...readyReplicas, which errors out when the field is not yet present)kubectl get svc smoke-test -n workspaces— expect port 80 targeting the container's first portkubectl get workspace smoke-test -n workspaces -o yamland confirmstatus.readyReplicasis 1 andstatus.conditionsreports readyAssert the API sees it: background-forward the API to 8888, then
curl -fsS "http://localhost:8888/v1/workspaces/smoke-test?namespace=workspaces"returns 200 with the workspace.Assert the proxy routes to it: background-forward the proxy to 8891, then
curl -fsS http://localhost:8891/proxy/workspaces/smoke-test/returns the whoami response body.Exercise stop/start. Stopping is annotation-driven — the controller scales the StatefulSet to 0 without deleting the CR:
curl -fsS -X POST "http://localhost:8888/v1/workspaces/smoke-test/stop?namespace=workspaces"→ StatefulSet replicas becomes 0 and the CR gains thekubeworkspaces.io/stoppedannotationcurl -fsS -X POST "http://localhost:8888/v1/workspaces/smoke-test/start?namespace=workspaces"→ replicas returns to 1 and the pod becomes Ready againClean up:
kubectl delete workspace smoke-test -n workspaces, then confirm the StatefulSet and Service are garbage-collected via owner references. Kill all port-forwards.Report every step as pass/fail with the observed value. If a step fails, dump
kubectl describe workspace smoke-test -n workspaces, the controller logs (kubectl logs -n kube-workspaces-system deploy/kube-workspaces-controller), and namespace events before drawing a conclusion.
| Document | Covers |
|---|---|
| docs/architecture.svg | Component diagram |
| docs/authentication.md | OIDC setup, roles, personal namespaces |
| docs/domains.md | Custom hostnames and ingress routing |
| docs/proxy.md | How workspace traffic is proxied |
| docs/security.md | ServiceAccount tokens, RBAC, what is deliberately not hardened |
| docs/testing.md | The test suite and how to run it |
| docs/releasing.md | Release procedure across the five repositories |
| CONTRIBUTING.md | Development setup |
Apache License 2.0