Kube Workspaces

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.

GitHub ReleaseLicensecontrollerapiproxyfrontendController CIAPI CIProxy CIFrontend CI

Architecture

Architecture diagram

Components

ComponentPathDescription
Controllercontroller/Kubernetes controller (kubebuilder) that reconciles Workspace, User, and AuthConfig CRs
APIapi/REST API service (Goa framework) providing workspace CRUD, volumes, images, auth, and a reverse proxy for workspace web UIs
Frontendfrontend/Next.js web UI with dashboard, workspace management, user management, namespace filtering, dark mode
Deploydeploy/Helm chart, Kustomize manifests, and ArgoCD Application for deployment

Features

  • 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

Quick Start

Prerequisites

  • 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.

Local cluster (kind)

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-frontend

Open 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.

Deploy to a 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.

Quick deploy with ArgoCD:

kubectl apply -f argocd/application-crds.yaml
kubectl apply -f argocd/application.yaml

Apply 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.

Quick deploy with Helm:

helm install kube-workspaces helm/kube-workspaces/ \
--namespace kube-workspaces-system --create-namespace

Or 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-namespace

Installing 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=false

Without 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.

Quick deploy with Kustomize:

kubectl apply --server-side -k kustomize/crds/
kubectl apply --server-side -k kustomize/base/

Docker Images

Released images are published to GHCR and are what the manifests reference by default — you do not need to build anything to deploy:

ComponentImage
controllerghcr.io/kube-workspaces/controller
apighcr.io/kube-workspaces/api
proxyghcr.io/kube-workspaces/proxy
frontendghcr.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):

forcin controller api proxy frontend;do
docker build -t "kube-workspaces-$c:dev""../$c"done

For 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

Authentication is opt-in and disabled by default. When disabled, the system operates without login — all users have full access (preserving backward compatibility).

Enabling Auth

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/v1alpha1kind: AuthConfigmetadata:
name: defaultspec:
enabled: trueoidc:
issuerURL: https://accounts.google.comclientID: <YOUR-GOOGLE-CLIENT-ID>clientSecret:
name: kube-workspaces-oidc-secretkey: client-secretsession:
signingKey:
name: kube-workspaces-session-secretkey: signing-keypersonalNamespaces:
enabled: truetemplate: "{{username}}"registration:
autoProvision: truedefaultRole: editoradminEmails:
- your-email@gmail.com

Create 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

Supported Identity Providers

  • Dex (recommended for multi-provider support) — supports LDAP, SAML, GitHub, GitLab, etc.
  • Okta — direct OIDC integration
  • Auth0 — direct OIDC integration
  • Any OIDC-compliant provider

User Management

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/v1alpha1kind: Usermetadata:
name: jane-doespec:
email: jane@example.comdisplayName: "Jane Doe"role: editornamespaceAccess:
- namespace: team-platformrole: editor

Roles

RolePermissions
adminFull access to all namespaces, user management, settings
editorCreate/edit/delete workspaces in assigned namespaces
viewerRead-only access to assigned namespaces

Personal 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 RoleBinding granting the user editor access
  • An optional ResourceQuota (if configured in AuthConfig)

CRDs

Workspace

The Workspace custom resource wraps a full Kubernetes PodSpec, giving complete flexibility over container configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
name: my-workspacenamespace: workspacesspec:
template:
spec:
containers:
- name: code-serverimage: codercom/code-server:latestargs: ["--bind-addr", "0.0.0.0:8080", "--auth", "none"]ports:
- containerPort: 8080resources:
requests:
cpu: "500m"memory: "512Mi"limits:
cpu: "2"memory: "2Gi"

Start/Stop

Workspaces are stopped by adding the annotation kubeworkspaces.io/stopped: "true", which sets the StatefulSet replicas to 0. Removing the annotation starts the workspace.

Image

Cluster-scoped CRD defining available workspace images with default configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Imagemetadata:
name: code-serverspec:
image: codercom/code-server:latestdisplayName: "Code Server (VS Code)"defaultPort: 8080icon: vscode

Available Images

The 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-images applies the full vendored images.yaml (currently 38 images). kustomize/base does not create any Image CRs on its own — this is a required separate step.

  • Helm: installs a curated set of 5 example images by default (installExampleImages: true). Set installCatalogImages: true to install the full catalog instead, or installExampleImages: false to install neither. Add your own images via the images: 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

Workspace Proxy

The API includes a built-in reverse proxy at /proxy/{namespace}/{name}/{path...} that provides direct browser access to running workspace web UIs.

Features

  • WebSocket support: Full WebSocket passthrough (needed for noVNC's websockify and 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.js or absolute paths) are caught via the Referer header 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

Local Access

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

API Endpoints

MethodPathDescription
GET/v1/workspacesList workspaces (supports ?namespace= filter)
GET/v1/workspaces/{name}Get workspace
POST/v1/workspacesCreate workspace
PUT/v1/workspaces/{name}Update workspace
DELETE/v1/workspaces/{name}Delete workspace
POST/v1/workspaces/{name}/startStart workspace
POST/v1/workspaces/{name}/stopStop workspace
GET/v1/workspaces/{name}/logsGet container logs
GET/v1/workspaces/{name}/eventsGet workspace events
GET/v1/workspaces/{name}/podGet pod details
GET/v1/workspaces/{name}/metricsGet pod metrics
GET/v1/volumesList volumes (supports ?namespace= filter)
POST/v1/volumesCreate volume
DELETE/v1/volumes/{name}Delete volume
GET/v1/imagesList available images
GET/v1/namespacesList namespaces
GET/healthzHealth check
GET/openapi3.jsonOpenAPI 3.0 spec (JSON)
GET/proxy/{ns}/{name}/{path...}Reverse proxy to workspace web UI
GET/auth/configPublic auth configuration
GET/auth/loginInitiate OIDC login
GET/auth/callbackOIDC callback
POST/auth/logoutClear session
GET/auth/meCurrent user info
GET/admin/usersList users (admin)
POST/admin/usersCreate user (admin)
PUT/admin/users/{name}Update user (admin)
DELETE/admin/users/{name}Delete user (admin)
GET/admin/auth-configGet AuthConfig (admin)
PUT/admin/auth-configUpdate AuthConfig (admin)
GET/admin/crds/definitionsList CRD definitions
GET/admin/crds/workspacesList raw workspace CRs

UI Pages

RouteDescription
/Dashboard with summary cards and workspace list
/loginSSO login page (shown when auth enabled)
/workspacesWorkspace table with status, actions
/workspaces/newCreate workspace form
/workspaces/{name}Workspace detail (Overview, Logs, Events, Metrics, YAML)
/volumesVolume list
/volumes/newCreate volume form
/imagesAvailable images catalog
/adminAdmin index (visible to admins only when auth enabled)
/admin/usersUser management (list, create, enable/disable, delete)
/admin/settingsAuth settings (OIDC config, namespaces, registration)
/admin/apiAPI documentation (Scalar)
/admin/imagesImage CR editor
/admin/crdsCRD browser

LLM Deployment Prompt

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 to the current kubectl context

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-context and stop and ask me if it is not what I expect.

  1. Clone https://github.com/kube-workspaces/deploy.git and work from the repo root.
  2. 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 KiB last-applied-configuration annotation limit, so plain kubectl apply -f fails.
  3. Install the components: kubectl apply --server-side -k kustomize/base/. The manifests already point at the published ghcr.io/kube-workspaces/* images, so do not build any images.
  4. Install the workspace image catalog: make install-images. This applies the cluster-scoped Image CRs from images.yaml. Skipping this leaves the UI catalog empty — kustomize/base does not create any Image CRs.

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 (expect kube-workspaces-controller, -api, -proxy, -frontend)
  • No container has restarted. Every pod must show 0 restarts and no CrashLoopBackOff: 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 & then curl -fsS http://localhost:8888/healthz must return {"status":"ok"}.
  • curl -fsS http://localhost:8888/v1/images lists the catalog entries you applied in step 4.
  • The frontend serves HTML: background-forward svc/kube-workspaces-frontend 3000:80 and check curl -fsS http://localhost:3000/ returns HTTP 200 with an HTML body. The frontend has no /healthz endpoint — / is its probe path.

If any deployment fails to become Available, diagnose before continuing: kubectl describe pod on the not-ready pod, kubectl logs for its containers, and kubectl 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 to a local kind cluster

Deploy kube-workspaces to a local kind cluster.

  1. kind create cluster --name kube-workspaces
  2. Clone https://github.com/kube-workspaces/deploy.git, then from the repo root run make install-crd && make deploy-kustomize && make install-images.
  3. Wait for readiness: kubectl wait --for=condition=Available deployment --all -n kube-workspaces-system --timeout=300s

Verify 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/healthz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-proxy 8891:80 &curl -fsS http://localhost:8891/readyz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-frontend 3000:80 &curl -fsS http://localhost:3000/ returns HTTP 200 and HTML

Kill every port-forward you started when done. Note that the Ingress in kustomize/base hardcodes ingressClassName: traefik and 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).

Verify an end-to-end workspace

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.

  1. Apply this Workspace CR. Note that a raw Workspace does not need a matching Image CR — Image CRs only populate the UI/API catalog and supply defaults at creation time through the API. Use traefik/whoami rather than a heavyweight IDE image so the pull is a few MB and the check is fast:

    apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
    name: smoke-testnamespace: workspacesspec:
    template:
    spec:
    containers:
    - name: whoamiimage: traefik/whoamiports:
    - containerPort: 80name: workspace-port

    The workspaces namespace already exists — kustomize/base creates it.

  2. 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 over kubectl 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 port
    • kubectl get workspace smoke-test -n workspaces -o yaml and confirm status.readyReplicas is 1 and status.conditions reports ready
  3. Assert 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.

  4. 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.

  5. 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 the kubeworkspaces.io/stopped annotation
    • curl -fsS -X POST "http://localhost:8888/v1/workspaces/smoke-test/start?namespace=workspaces" → replicas returns to 1 and the pod becomes Ready again
  6. Clean 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.

Documentation

DocumentCovers
docs/architecture.svgComponent diagram
docs/authentication.mdOIDC setup, roles, personal namespaces
docs/domains.mdCustom hostnames and ingress routing
docs/proxy.mdHow workspace traffic is proxied
docs/security.mdServiceAccount tokens, RBAC, what is deliberately not hardened
docs/testing.mdThe test suite and how to run it
docs/releasing.mdRelease procedure across the five repositories
CONTRIBUTING.mdDevelopment setup

License

Apache License 2.0

About

Deployment manifests (Helm, Kustomize, ArgoCD) and documentation for kube-workspaces

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Kube Workspaces

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.

GitHub ReleaseLicensecontrollerapiproxyfrontendController CIAPI CIProxy CIFrontend CI

Architecture

Architecture diagram

Components

ComponentPathDescription
Controllercontroller/Kubernetes controller (kubebuilder) that reconciles Workspace, User, and AuthConfig CRs
APIapi/REST API service (Goa framework) providing workspace CRUD, volumes, images, auth, and a reverse proxy for workspace web UIs
Frontendfrontend/Next.js web UI with dashboard, workspace management, user management, namespace filtering, dark mode
Deploydeploy/Helm chart, Kustomize manifests, and ArgoCD Application for deployment

Features

  • 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

Quick Start

Prerequisites

  • 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.

Local cluster (kind)

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-frontend

Open 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.

Deploy to a 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.

Quick deploy with ArgoCD:

kubectl apply -f argocd/application-crds.yaml
kubectl apply -f argocd/application.yaml

Apply 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.

Quick deploy with Helm:

helm install kube-workspaces helm/kube-workspaces/ \
--namespace kube-workspaces-system --create-namespace

Or 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-namespace

Installing 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=false

Without 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.

Quick deploy with Kustomize:

kubectl apply --server-side -k kustomize/crds/
kubectl apply --server-side -k kustomize/base/

Docker Images

Released images are published to GHCR and are what the manifests reference by default — you do not need to build anything to deploy:

ComponentImage
controllerghcr.io/kube-workspaces/controller
apighcr.io/kube-workspaces/api
proxyghcr.io/kube-workspaces/proxy
frontendghcr.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):

forcin controller api proxy frontend;do
docker build -t "kube-workspaces-$c:dev""../$c"done

For 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

Authentication is opt-in and disabled by default. When disabled, the system operates without login — all users have full access (preserving backward compatibility).

Enabling Auth

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/v1alpha1kind: AuthConfigmetadata:
name: defaultspec:
enabled: trueoidc:
issuerURL: https://accounts.google.comclientID: <YOUR-GOOGLE-CLIENT-ID>clientSecret:
name: kube-workspaces-oidc-secretkey: client-secretsession:
signingKey:
name: kube-workspaces-session-secretkey: signing-keypersonalNamespaces:
enabled: truetemplate: "{{username}}"registration:
autoProvision: truedefaultRole: editoradminEmails:
- your-email@gmail.com

Create 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

Supported Identity Providers

  • Dex (recommended for multi-provider support) — supports LDAP, SAML, GitHub, GitLab, etc.
  • Okta — direct OIDC integration
  • Auth0 — direct OIDC integration
  • Any OIDC-compliant provider

User Management

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/v1alpha1kind: Usermetadata:
name: jane-doespec:
email: jane@example.comdisplayName: "Jane Doe"role: editornamespaceAccess:
- namespace: team-platformrole: editor

Roles

RolePermissions
adminFull access to all namespaces, user management, settings
editorCreate/edit/delete workspaces in assigned namespaces
viewerRead-only access to assigned namespaces

Personal 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 RoleBinding granting the user editor access
  • An optional ResourceQuota (if configured in AuthConfig)

CRDs

Workspace

The Workspace custom resource wraps a full Kubernetes PodSpec, giving complete flexibility over container configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
name: my-workspacenamespace: workspacesspec:
template:
spec:
containers:
- name: code-serverimage: codercom/code-server:latestargs: ["--bind-addr", "0.0.0.0:8080", "--auth", "none"]ports:
- containerPort: 8080resources:
requests:
cpu: "500m"memory: "512Mi"limits:
cpu: "2"memory: "2Gi"

Start/Stop

Workspaces are stopped by adding the annotation kubeworkspaces.io/stopped: "true", which sets the StatefulSet replicas to 0. Removing the annotation starts the workspace.

Image

Cluster-scoped CRD defining available workspace images with default configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Imagemetadata:
name: code-serverspec:
image: codercom/code-server:latestdisplayName: "Code Server (VS Code)"defaultPort: 8080icon: vscode

Available Images

The 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-images applies the full vendored images.yaml (currently 38 images). kustomize/base does not create any Image CRs on its own — this is a required separate step.

  • Helm: installs a curated set of 5 example images by default (installExampleImages: true). Set installCatalogImages: true to install the full catalog instead, or installExampleImages: false to install neither. Add your own images via the images: 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

Workspace Proxy

The API includes a built-in reverse proxy at /proxy/{namespace}/{name}/{path...} that provides direct browser access to running workspace web UIs.

Features

  • WebSocket support: Full WebSocket passthrough (needed for noVNC's websockify and 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.js or absolute paths) are caught via the Referer header 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

Local Access

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

API Endpoints

MethodPathDescription
GET/v1/workspacesList workspaces (supports ?namespace= filter)
GET/v1/workspaces/{name}Get workspace
POST/v1/workspacesCreate workspace
PUT/v1/workspaces/{name}Update workspace
DELETE/v1/workspaces/{name}Delete workspace
POST/v1/workspaces/{name}/startStart workspace
POST/v1/workspaces/{name}/stopStop workspace
GET/v1/workspaces/{name}/logsGet container logs
GET/v1/workspaces/{name}/eventsGet workspace events
GET/v1/workspaces/{name}/podGet pod details
GET/v1/workspaces/{name}/metricsGet pod metrics
GET/v1/volumesList volumes (supports ?namespace= filter)
POST/v1/volumesCreate volume
DELETE/v1/volumes/{name}Delete volume
GET/v1/imagesList available images
GET/v1/namespacesList namespaces
GET/healthzHealth check
GET/openapi3.jsonOpenAPI 3.0 spec (JSON)
GET/proxy/{ns}/{name}/{path...}Reverse proxy to workspace web UI
GET/auth/configPublic auth configuration
GET/auth/loginInitiate OIDC login
GET/auth/callbackOIDC callback
POST/auth/logoutClear session
GET/auth/meCurrent user info
GET/admin/usersList users (admin)
POST/admin/usersCreate user (admin)
PUT/admin/users/{name}Update user (admin)
DELETE/admin/users/{name}Delete user (admin)
GET/admin/auth-configGet AuthConfig (admin)
PUT/admin/auth-configUpdate AuthConfig (admin)
GET/admin/crds/definitionsList CRD definitions
GET/admin/crds/workspacesList raw workspace CRs

UI Pages

RouteDescription
/Dashboard with summary cards and workspace list
/loginSSO login page (shown when auth enabled)
/workspacesWorkspace table with status, actions
/workspaces/newCreate workspace form
/workspaces/{name}Workspace detail (Overview, Logs, Events, Metrics, YAML)
/volumesVolume list
/volumes/newCreate volume form
/imagesAvailable images catalog
/adminAdmin index (visible to admins only when auth enabled)
/admin/usersUser management (list, create, enable/disable, delete)
/admin/settingsAuth settings (OIDC config, namespaces, registration)
/admin/apiAPI documentation (Scalar)
/admin/imagesImage CR editor
/admin/crdsCRD browser

LLM Deployment Prompt

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 to the current kubectl context

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-context and stop and ask me if it is not what I expect.

  1. Clone https://github.com/kube-workspaces/deploy.git and work from the repo root.
  2. 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 KiB last-applied-configuration annotation limit, so plain kubectl apply -f fails.
  3. Install the components: kubectl apply --server-side -k kustomize/base/. The manifests already point at the published ghcr.io/kube-workspaces/* images, so do not build any images.
  4. Install the workspace image catalog: make install-images. This applies the cluster-scoped Image CRs from images.yaml. Skipping this leaves the UI catalog empty — kustomize/base does not create any Image CRs.

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 (expect kube-workspaces-controller, -api, -proxy, -frontend)
  • No container has restarted. Every pod must show 0 restarts and no CrashLoopBackOff: 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 & then curl -fsS http://localhost:8888/healthz must return {"status":"ok"}.
  • curl -fsS http://localhost:8888/v1/images lists the catalog entries you applied in step 4.
  • The frontend serves HTML: background-forward svc/kube-workspaces-frontend 3000:80 and check curl -fsS http://localhost:3000/ returns HTTP 200 with an HTML body. The frontend has no /healthz endpoint — / is its probe path.

If any deployment fails to become Available, diagnose before continuing: kubectl describe pod on the not-ready pod, kubectl logs for its containers, and kubectl 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 to a local kind cluster

Deploy kube-workspaces to a local kind cluster.

  1. kind create cluster --name kube-workspaces
  2. Clone https://github.com/kube-workspaces/deploy.git, then from the repo root run make install-crd && make deploy-kustomize && make install-images.
  3. Wait for readiness: kubectl wait --for=condition=Available deployment --all -n kube-workspaces-system --timeout=300s

Verify 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/healthz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-proxy 8891:80 &curl -fsS http://localhost:8891/readyz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-frontend 3000:80 &curl -fsS http://localhost:3000/ returns HTTP 200 and HTML

Kill every port-forward you started when done. Note that the Ingress in kustomize/base hardcodes ingressClassName: traefik and 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).

Verify an end-to-end workspace

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.

  1. Apply this Workspace CR. Note that a raw Workspace does not need a matching Image CR — Image CRs only populate the UI/API catalog and supply defaults at creation time through the API. Use traefik/whoami rather than a heavyweight IDE image so the pull is a few MB and the check is fast:

    apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
    name: smoke-testnamespace: workspacesspec:
    template:
    spec:
    containers:
    - name: whoamiimage: traefik/whoamiports:
    - containerPort: 80name: workspace-port

    The workspaces namespace already exists — kustomize/base creates it.

  2. 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 over kubectl 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 port
    • kubectl get workspace smoke-test -n workspaces -o yaml and confirm status.readyReplicas is 1 and status.conditions reports ready
  3. Assert 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.

  4. 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.

  5. 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 the kubeworkspaces.io/stopped annotation
    • curl -fsS -X POST "http://localhost:8888/v1/workspaces/smoke-test/start?namespace=workspaces" → replicas returns to 1 and the pod becomes Ready again
  6. Clean 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.

Documentation

DocumentCovers
docs/architecture.svgComponent diagram
docs/authentication.mdOIDC setup, roles, personal namespaces
docs/domains.mdCustom hostnames and ingress routing
docs/proxy.mdHow workspace traffic is proxied
docs/security.mdServiceAccount tokens, RBAC, what is deliberately not hardened
docs/testing.mdThe test suite and how to run it
docs/releasing.mdRelease procedure across the five repositories
CONTRIBUTING.mdDevelopment setup

License

Apache License 2.0

About

Deployment manifests (Helm, Kustomize, ArgoCD) and documentation for kube-workspaces

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Kube Workspaces

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.

GitHub ReleaseLicensecontrollerapiproxyfrontendController CIAPI CIProxy CIFrontend CI

Architecture

Architecture diagram

Components

ComponentPathDescription
Controllercontroller/Kubernetes controller (kubebuilder) that reconciles Workspace, User, and AuthConfig CRs
APIapi/REST API service (Goa framework) providing workspace CRUD, volumes, images, auth, and a reverse proxy for workspace web UIs
Frontendfrontend/Next.js web UI with dashboard, workspace management, user management, namespace filtering, dark mode
Deploydeploy/Helm chart, Kustomize manifests, and ArgoCD Application for deployment

Features

  • 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

Quick Start

Prerequisites

  • 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.

Local cluster (kind)

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-frontend

Open 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.

Deploy to a 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.

Quick deploy with ArgoCD:

kubectl apply -f argocd/application-crds.yaml
kubectl apply -f argocd/application.yaml

Apply 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.

Quick deploy with Helm:

helm install kube-workspaces helm/kube-workspaces/ \
--namespace kube-workspaces-system --create-namespace

Or 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-namespace

Installing 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=false

Without 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.

Quick deploy with Kustomize:

kubectl apply --server-side -k kustomize/crds/
kubectl apply --server-side -k kustomize/base/

Docker Images

Released images are published to GHCR and are what the manifests reference by default — you do not need to build anything to deploy:

ComponentImage
controllerghcr.io/kube-workspaces/controller
apighcr.io/kube-workspaces/api
proxyghcr.io/kube-workspaces/proxy
frontendghcr.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):

forcin controller api proxy frontend;do
docker build -t "kube-workspaces-$c:dev""../$c"done

For 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

Authentication is opt-in and disabled by default. When disabled, the system operates without login — all users have full access (preserving backward compatibility).

Enabling Auth

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/v1alpha1kind: AuthConfigmetadata:
name: defaultspec:
enabled: trueoidc:
issuerURL: https://accounts.google.comclientID: <YOUR-GOOGLE-CLIENT-ID>clientSecret:
name: kube-workspaces-oidc-secretkey: client-secretsession:
signingKey:
name: kube-workspaces-session-secretkey: signing-keypersonalNamespaces:
enabled: truetemplate: "{{username}}"registration:
autoProvision: truedefaultRole: editoradminEmails:
- your-email@gmail.com

Create 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

Supported Identity Providers

  • Dex (recommended for multi-provider support) — supports LDAP, SAML, GitHub, GitLab, etc.
  • Okta — direct OIDC integration
  • Auth0 — direct OIDC integration
  • Any OIDC-compliant provider

User Management

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/v1alpha1kind: Usermetadata:
name: jane-doespec:
email: jane@example.comdisplayName: "Jane Doe"role: editornamespaceAccess:
- namespace: team-platformrole: editor

Roles

RolePermissions
adminFull access to all namespaces, user management, settings
editorCreate/edit/delete workspaces in assigned namespaces
viewerRead-only access to assigned namespaces

Personal 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 RoleBinding granting the user editor access
  • An optional ResourceQuota (if configured in AuthConfig)

CRDs

Workspace

The Workspace custom resource wraps a full Kubernetes PodSpec, giving complete flexibility over container configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
name: my-workspacenamespace: workspacesspec:
template:
spec:
containers:
- name: code-serverimage: codercom/code-server:latestargs: ["--bind-addr", "0.0.0.0:8080", "--auth", "none"]ports:
- containerPort: 8080resources:
requests:
cpu: "500m"memory: "512Mi"limits:
cpu: "2"memory: "2Gi"

Start/Stop

Workspaces are stopped by adding the annotation kubeworkspaces.io/stopped: "true", which sets the StatefulSet replicas to 0. Removing the annotation starts the workspace.

Image

Cluster-scoped CRD defining available workspace images with default configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Imagemetadata:
name: code-serverspec:
image: codercom/code-server:latestdisplayName: "Code Server (VS Code)"defaultPort: 8080icon: vscode

Available Images

The 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-images applies the full vendored images.yaml (currently 38 images). kustomize/base does not create any Image CRs on its own — this is a required separate step.

  • Helm: installs a curated set of 5 example images by default (installExampleImages: true). Set installCatalogImages: true to install the full catalog instead, or installExampleImages: false to install neither. Add your own images via the images: 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

Workspace Proxy

The API includes a built-in reverse proxy at /proxy/{namespace}/{name}/{path...} that provides direct browser access to running workspace web UIs.

Features

  • WebSocket support: Full WebSocket passthrough (needed for noVNC's websockify and 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.js or absolute paths) are caught via the Referer header 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

Local Access

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

API Endpoints

MethodPathDescription
GET/v1/workspacesList workspaces (supports ?namespace= filter)
GET/v1/workspaces/{name}Get workspace
POST/v1/workspacesCreate workspace
PUT/v1/workspaces/{name}Update workspace
DELETE/v1/workspaces/{name}Delete workspace
POST/v1/workspaces/{name}/startStart workspace
POST/v1/workspaces/{name}/stopStop workspace
GET/v1/workspaces/{name}/logsGet container logs
GET/v1/workspaces/{name}/eventsGet workspace events
GET/v1/workspaces/{name}/podGet pod details
GET/v1/workspaces/{name}/metricsGet pod metrics
GET/v1/volumesList volumes (supports ?namespace= filter)
POST/v1/volumesCreate volume
DELETE/v1/volumes/{name}Delete volume
GET/v1/imagesList available images
GET/v1/namespacesList namespaces
GET/healthzHealth check
GET/openapi3.jsonOpenAPI 3.0 spec (JSON)
GET/proxy/{ns}/{name}/{path...}Reverse proxy to workspace web UI
GET/auth/configPublic auth configuration
GET/auth/loginInitiate OIDC login
GET/auth/callbackOIDC callback
POST/auth/logoutClear session
GET/auth/meCurrent user info
GET/admin/usersList users (admin)
POST/admin/usersCreate user (admin)
PUT/admin/users/{name}Update user (admin)
DELETE/admin/users/{name}Delete user (admin)
GET/admin/auth-configGet AuthConfig (admin)
PUT/admin/auth-configUpdate AuthConfig (admin)
GET/admin/crds/definitionsList CRD definitions
GET/admin/crds/workspacesList raw workspace CRs

UI Pages

RouteDescription
/Dashboard with summary cards and workspace list
/loginSSO login page (shown when auth enabled)
/workspacesWorkspace table with status, actions
/workspaces/newCreate workspace form
/workspaces/{name}Workspace detail (Overview, Logs, Events, Metrics, YAML)
/volumesVolume list
/volumes/newCreate volume form
/imagesAvailable images catalog
/adminAdmin index (visible to admins only when auth enabled)
/admin/usersUser management (list, create, enable/disable, delete)
/admin/settingsAuth settings (OIDC config, namespaces, registration)
/admin/apiAPI documentation (Scalar)
/admin/imagesImage CR editor
/admin/crdsCRD browser

LLM Deployment Prompt

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 to the current kubectl context

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-context and stop and ask me if it is not what I expect.

  1. Clone https://github.com/kube-workspaces/deploy.git and work from the repo root.
  2. 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 KiB last-applied-configuration annotation limit, so plain kubectl apply -f fails.
  3. Install the components: kubectl apply --server-side -k kustomize/base/. The manifests already point at the published ghcr.io/kube-workspaces/* images, so do not build any images.
  4. Install the workspace image catalog: make install-images. This applies the cluster-scoped Image CRs from images.yaml. Skipping this leaves the UI catalog empty — kustomize/base does not create any Image CRs.

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 (expect kube-workspaces-controller, -api, -proxy, -frontend)
  • No container has restarted. Every pod must show 0 restarts and no CrashLoopBackOff: 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 & then curl -fsS http://localhost:8888/healthz must return {"status":"ok"}.
  • curl -fsS http://localhost:8888/v1/images lists the catalog entries you applied in step 4.
  • The frontend serves HTML: background-forward svc/kube-workspaces-frontend 3000:80 and check curl -fsS http://localhost:3000/ returns HTTP 200 with an HTML body. The frontend has no /healthz endpoint — / is its probe path.

If any deployment fails to become Available, diagnose before continuing: kubectl describe pod on the not-ready pod, kubectl logs for its containers, and kubectl 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 to a local kind cluster

Deploy kube-workspaces to a local kind cluster.

  1. kind create cluster --name kube-workspaces
  2. Clone https://github.com/kube-workspaces/deploy.git, then from the repo root run make install-crd && make deploy-kustomize && make install-images.
  3. Wait for readiness: kubectl wait --for=condition=Available deployment --all -n kube-workspaces-system --timeout=300s

Verify 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/healthz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-proxy 8891:80 &curl -fsS http://localhost:8891/readyz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-frontend 3000:80 &curl -fsS http://localhost:3000/ returns HTTP 200 and HTML

Kill every port-forward you started when done. Note that the Ingress in kustomize/base hardcodes ingressClassName: traefik and 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).

Verify an end-to-end workspace

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.

  1. Apply this Workspace CR. Note that a raw Workspace does not need a matching Image CR — Image CRs only populate the UI/API catalog and supply defaults at creation time through the API. Use traefik/whoami rather than a heavyweight IDE image so the pull is a few MB and the check is fast:

    apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
    name: smoke-testnamespace: workspacesspec:
    template:
    spec:
    containers:
    - name: whoamiimage: traefik/whoamiports:
    - containerPort: 80name: workspace-port

    The workspaces namespace already exists — kustomize/base creates it.

  2. 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 over kubectl 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 port
    • kubectl get workspace smoke-test -n workspaces -o yaml and confirm status.readyReplicas is 1 and status.conditions reports ready
  3. Assert 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.

  4. 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.

  5. 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 the kubeworkspaces.io/stopped annotation
    • curl -fsS -X POST "http://localhost:8888/v1/workspaces/smoke-test/start?namespace=workspaces" → replicas returns to 1 and the pod becomes Ready again
  6. Clean 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.

Documentation

DocumentCovers
docs/architecture.svgComponent diagram
docs/authentication.mdOIDC setup, roles, personal namespaces
docs/domains.mdCustom hostnames and ingress routing
docs/proxy.mdHow workspace traffic is proxied
docs/security.mdServiceAccount tokens, RBAC, what is deliberately not hardened
docs/testing.mdThe test suite and how to run it
docs/releasing.mdRelease procedure across the five repositories
CONTRIBUTING.mdDevelopment setup

License

Apache License 2.0

About

Deployment manifests (Helm, Kustomize, ArgoCD) and documentation for kube-workspaces

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Kube Workspaces

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.

GitHub ReleaseLicensecontrollerapiproxyfrontendController CIAPI CIProxy CIFrontend CI

Architecture

Architecture diagram

Components

ComponentPathDescription
Controllercontroller/Kubernetes controller (kubebuilder) that reconciles Workspace, User, and AuthConfig CRs
APIapi/REST API service (Goa framework) providing workspace CRUD, volumes, images, auth, and a reverse proxy for workspace web UIs
Frontendfrontend/Next.js web UI with dashboard, workspace management, user management, namespace filtering, dark mode
Deploydeploy/Helm chart, Kustomize manifests, and ArgoCD Application for deployment

Features

  • 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

Quick Start

Prerequisites

  • 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.

Local cluster (kind)

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-frontend

Open 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.

Deploy to a 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.

Quick deploy with ArgoCD:

kubectl apply -f argocd/application-crds.yaml
kubectl apply -f argocd/application.yaml

Apply 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.

Quick deploy with Helm:

helm install kube-workspaces helm/kube-workspaces/ \
--namespace kube-workspaces-system --create-namespace

Or 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-namespace

Installing 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=false

Without 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.

Quick deploy with Kustomize:

kubectl apply --server-side -k kustomize/crds/
kubectl apply --server-side -k kustomize/base/

Docker Images

Released images are published to GHCR and are what the manifests reference by default — you do not need to build anything to deploy:

ComponentImage
controllerghcr.io/kube-workspaces/controller
apighcr.io/kube-workspaces/api
proxyghcr.io/kube-workspaces/proxy
frontendghcr.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):

forcin controller api proxy frontend;do
docker build -t "kube-workspaces-$c:dev""../$c"done

For 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

Authentication is opt-in and disabled by default. When disabled, the system operates without login — all users have full access (preserving backward compatibility).

Enabling Auth

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/v1alpha1kind: AuthConfigmetadata:
name: defaultspec:
enabled: trueoidc:
issuerURL: https://accounts.google.comclientID: <YOUR-GOOGLE-CLIENT-ID>clientSecret:
name: kube-workspaces-oidc-secretkey: client-secretsession:
signingKey:
name: kube-workspaces-session-secretkey: signing-keypersonalNamespaces:
enabled: truetemplate: "{{username}}"registration:
autoProvision: truedefaultRole: editoradminEmails:
- your-email@gmail.com

Create 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

Supported Identity Providers

  • Dex (recommended for multi-provider support) — supports LDAP, SAML, GitHub, GitLab, etc.
  • Okta — direct OIDC integration
  • Auth0 — direct OIDC integration
  • Any OIDC-compliant provider

User Management

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/v1alpha1kind: Usermetadata:
name: jane-doespec:
email: jane@example.comdisplayName: "Jane Doe"role: editornamespaceAccess:
- namespace: team-platformrole: editor

Roles

RolePermissions
adminFull access to all namespaces, user management, settings
editorCreate/edit/delete workspaces in assigned namespaces
viewerRead-only access to assigned namespaces

Personal 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 RoleBinding granting the user editor access
  • An optional ResourceQuota (if configured in AuthConfig)

CRDs

Workspace

The Workspace custom resource wraps a full Kubernetes PodSpec, giving complete flexibility over container configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
name: my-workspacenamespace: workspacesspec:
template:
spec:
containers:
- name: code-serverimage: codercom/code-server:latestargs: ["--bind-addr", "0.0.0.0:8080", "--auth", "none"]ports:
- containerPort: 8080resources:
requests:
cpu: "500m"memory: "512Mi"limits:
cpu: "2"memory: "2Gi"

Start/Stop

Workspaces are stopped by adding the annotation kubeworkspaces.io/stopped: "true", which sets the StatefulSet replicas to 0. Removing the annotation starts the workspace.

Image

Cluster-scoped CRD defining available workspace images with default configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Imagemetadata:
name: code-serverspec:
image: codercom/code-server:latestdisplayName: "Code Server (VS Code)"defaultPort: 8080icon: vscode

Available Images

The 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-images applies the full vendored images.yaml (currently 38 images). kustomize/base does not create any Image CRs on its own — this is a required separate step.

  • Helm: installs a curated set of 5 example images by default (installExampleImages: true). Set installCatalogImages: true to install the full catalog instead, or installExampleImages: false to install neither. Add your own images via the images: 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

Workspace Proxy

The API includes a built-in reverse proxy at /proxy/{namespace}/{name}/{path...} that provides direct browser access to running workspace web UIs.

Features

  • WebSocket support: Full WebSocket passthrough (needed for noVNC's websockify and 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.js or absolute paths) are caught via the Referer header 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

Local Access

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

API Endpoints

MethodPathDescription
GET/v1/workspacesList workspaces (supports ?namespace= filter)
GET/v1/workspaces/{name}Get workspace
POST/v1/workspacesCreate workspace
PUT/v1/workspaces/{name}Update workspace
DELETE/v1/workspaces/{name}Delete workspace
POST/v1/workspaces/{name}/startStart workspace
POST/v1/workspaces/{name}/stopStop workspace
GET/v1/workspaces/{name}/logsGet container logs
GET/v1/workspaces/{name}/eventsGet workspace events
GET/v1/workspaces/{name}/podGet pod details
GET/v1/workspaces/{name}/metricsGet pod metrics
GET/v1/volumesList volumes (supports ?namespace= filter)
POST/v1/volumesCreate volume
DELETE/v1/volumes/{name}Delete volume
GET/v1/imagesList available images
GET/v1/namespacesList namespaces
GET/healthzHealth check
GET/openapi3.jsonOpenAPI 3.0 spec (JSON)
GET/proxy/{ns}/{name}/{path...}Reverse proxy to workspace web UI
GET/auth/configPublic auth configuration
GET/auth/loginInitiate OIDC login
GET/auth/callbackOIDC callback
POST/auth/logoutClear session
GET/auth/meCurrent user info
GET/admin/usersList users (admin)
POST/admin/usersCreate user (admin)
PUT/admin/users/{name}Update user (admin)
DELETE/admin/users/{name}Delete user (admin)
GET/admin/auth-configGet AuthConfig (admin)
PUT/admin/auth-configUpdate AuthConfig (admin)
GET/admin/crds/definitionsList CRD definitions
GET/admin/crds/workspacesList raw workspace CRs

UI Pages

RouteDescription
/Dashboard with summary cards and workspace list
/loginSSO login page (shown when auth enabled)
/workspacesWorkspace table with status, actions
/workspaces/newCreate workspace form
/workspaces/{name}Workspace detail (Overview, Logs, Events, Metrics, YAML)
/volumesVolume list
/volumes/newCreate volume form
/imagesAvailable images catalog
/adminAdmin index (visible to admins only when auth enabled)
/admin/usersUser management (list, create, enable/disable, delete)
/admin/settingsAuth settings (OIDC config, namespaces, registration)
/admin/apiAPI documentation (Scalar)
/admin/imagesImage CR editor
/admin/crdsCRD browser

LLM Deployment Prompt

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 to the current kubectl context

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-context and stop and ask me if it is not what I expect.

  1. Clone https://github.com/kube-workspaces/deploy.git and work from the repo root.
  2. 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 KiB last-applied-configuration annotation limit, so plain kubectl apply -f fails.
  3. Install the components: kubectl apply --server-side -k kustomize/base/. The manifests already point at the published ghcr.io/kube-workspaces/* images, so do not build any images.
  4. Install the workspace image catalog: make install-images. This applies the cluster-scoped Image CRs from images.yaml. Skipping this leaves the UI catalog empty — kustomize/base does not create any Image CRs.

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 (expect kube-workspaces-controller, -api, -proxy, -frontend)
  • No container has restarted. Every pod must show 0 restarts and no CrashLoopBackOff: 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 & then curl -fsS http://localhost:8888/healthz must return {"status":"ok"}.
  • curl -fsS http://localhost:8888/v1/images lists the catalog entries you applied in step 4.
  • The frontend serves HTML: background-forward svc/kube-workspaces-frontend 3000:80 and check curl -fsS http://localhost:3000/ returns HTTP 200 with an HTML body. The frontend has no /healthz endpoint — / is its probe path.

If any deployment fails to become Available, diagnose before continuing: kubectl describe pod on the not-ready pod, kubectl logs for its containers, and kubectl 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 to a local kind cluster

Deploy kube-workspaces to a local kind cluster.

  1. kind create cluster --name kube-workspaces
  2. Clone https://github.com/kube-workspaces/deploy.git, then from the repo root run make install-crd && make deploy-kustomize && make install-images.
  3. Wait for readiness: kubectl wait --for=condition=Available deployment --all -n kube-workspaces-system --timeout=300s

Verify 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/healthz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-proxy 8891:80 &curl -fsS http://localhost:8891/readyz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-frontend 3000:80 &curl -fsS http://localhost:3000/ returns HTTP 200 and HTML

Kill every port-forward you started when done. Note that the Ingress in kustomize/base hardcodes ingressClassName: traefik and 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).

Verify an end-to-end workspace

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.

  1. Apply this Workspace CR. Note that a raw Workspace does not need a matching Image CR — Image CRs only populate the UI/API catalog and supply defaults at creation time through the API. Use traefik/whoami rather than a heavyweight IDE image so the pull is a few MB and the check is fast:

    apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
    name: smoke-testnamespace: workspacesspec:
    template:
    spec:
    containers:
    - name: whoamiimage: traefik/whoamiports:
    - containerPort: 80name: workspace-port

    The workspaces namespace already exists — kustomize/base creates it.

  2. 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 over kubectl 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 port
    • kubectl get workspace smoke-test -n workspaces -o yaml and confirm status.readyReplicas is 1 and status.conditions reports ready
  3. Assert 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.

  4. 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.

  5. 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 the kubeworkspaces.io/stopped annotation
    • curl -fsS -X POST "http://localhost:8888/v1/workspaces/smoke-test/start?namespace=workspaces" → replicas returns to 1 and the pod becomes Ready again
  6. Clean 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.

Documentation

DocumentCovers
docs/architecture.svgComponent diagram
docs/authentication.mdOIDC setup, roles, personal namespaces
docs/domains.mdCustom hostnames and ingress routing
docs/proxy.mdHow workspace traffic is proxied
docs/security.mdServiceAccount tokens, RBAC, what is deliberately not hardened
docs/testing.mdThe test suite and how to run it
docs/releasing.mdRelease procedure across the five repositories
CONTRIBUTING.mdDevelopment setup

License

Apache License 2.0

About

Deployment manifests (Helm, Kustomize, ArgoCD) and documentation for kube-workspaces

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Kube Workspaces

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.

GitHub ReleaseLicensecontrollerapiproxyfrontendController CIAPI CIProxy CIFrontend CI

Architecture

Architecture diagram

Components

ComponentPathDescription
Controllercontroller/Kubernetes controller (kubebuilder) that reconciles Workspace, User, and AuthConfig CRs
APIapi/REST API service (Goa framework) providing workspace CRUD, volumes, images, auth, and a reverse proxy for workspace web UIs
Frontendfrontend/Next.js web UI with dashboard, workspace management, user management, namespace filtering, dark mode
Deploydeploy/Helm chart, Kustomize manifests, and ArgoCD Application for deployment

Features

  • 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

Quick Start

Prerequisites

  • 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.

Local cluster (kind)

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-frontend

Open 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.

Deploy to a 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.

Quick deploy with ArgoCD:

kubectl apply -f argocd/application-crds.yaml
kubectl apply -f argocd/application.yaml

Apply 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.

Quick deploy with Helm:

helm install kube-workspaces helm/kube-workspaces/ \
--namespace kube-workspaces-system --create-namespace

Or 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-namespace

Installing 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=false

Without 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.

Quick deploy with Kustomize:

kubectl apply --server-side -k kustomize/crds/
kubectl apply --server-side -k kustomize/base/

Docker Images

Released images are published to GHCR and are what the manifests reference by default — you do not need to build anything to deploy:

ComponentImage
controllerghcr.io/kube-workspaces/controller
apighcr.io/kube-workspaces/api
proxyghcr.io/kube-workspaces/proxy
frontendghcr.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):

forcin controller api proxy frontend;do
docker build -t "kube-workspaces-$c:dev""../$c"done

For 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

Authentication is opt-in and disabled by default. When disabled, the system operates without login — all users have full access (preserving backward compatibility).

Enabling Auth

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/v1alpha1kind: AuthConfigmetadata:
name: defaultspec:
enabled: trueoidc:
issuerURL: https://accounts.google.comclientID: <YOUR-GOOGLE-CLIENT-ID>clientSecret:
name: kube-workspaces-oidc-secretkey: client-secretsession:
signingKey:
name: kube-workspaces-session-secretkey: signing-keypersonalNamespaces:
enabled: truetemplate: "{{username}}"registration:
autoProvision: truedefaultRole: editoradminEmails:
- your-email@gmail.com

Create 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

Supported Identity Providers

  • Dex (recommended for multi-provider support) — supports LDAP, SAML, GitHub, GitLab, etc.
  • Okta — direct OIDC integration
  • Auth0 — direct OIDC integration
  • Any OIDC-compliant provider

User Management

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/v1alpha1kind: Usermetadata:
name: jane-doespec:
email: jane@example.comdisplayName: "Jane Doe"role: editornamespaceAccess:
- namespace: team-platformrole: editor

Roles

RolePermissions
adminFull access to all namespaces, user management, settings
editorCreate/edit/delete workspaces in assigned namespaces
viewerRead-only access to assigned namespaces

Personal 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 RoleBinding granting the user editor access
  • An optional ResourceQuota (if configured in AuthConfig)

CRDs

Workspace

The Workspace custom resource wraps a full Kubernetes PodSpec, giving complete flexibility over container configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
name: my-workspacenamespace: workspacesspec:
template:
spec:
containers:
- name: code-serverimage: codercom/code-server:latestargs: ["--bind-addr", "0.0.0.0:8080", "--auth", "none"]ports:
- containerPort: 8080resources:
requests:
cpu: "500m"memory: "512Mi"limits:
cpu: "2"memory: "2Gi"

Start/Stop

Workspaces are stopped by adding the annotation kubeworkspaces.io/stopped: "true", which sets the StatefulSet replicas to 0. Removing the annotation starts the workspace.

Image

Cluster-scoped CRD defining available workspace images with default configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Imagemetadata:
name: code-serverspec:
image: codercom/code-server:latestdisplayName: "Code Server (VS Code)"defaultPort: 8080icon: vscode

Available Images

The 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-images applies the full vendored images.yaml (currently 38 images). kustomize/base does not create any Image CRs on its own — this is a required separate step.

  • Helm: installs a curated set of 5 example images by default (installExampleImages: true). Set installCatalogImages: true to install the full catalog instead, or installExampleImages: false to install neither. Add your own images via the images: 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

Workspace Proxy

The API includes a built-in reverse proxy at /proxy/{namespace}/{name}/{path...} that provides direct browser access to running workspace web UIs.

Features

  • WebSocket support: Full WebSocket passthrough (needed for noVNC's websockify and 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.js or absolute paths) are caught via the Referer header 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

Local Access

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

API Endpoints

MethodPathDescription
GET/v1/workspacesList workspaces (supports ?namespace= filter)
GET/v1/workspaces/{name}Get workspace
POST/v1/workspacesCreate workspace
PUT/v1/workspaces/{name}Update workspace
DELETE/v1/workspaces/{name}Delete workspace
POST/v1/workspaces/{name}/startStart workspace
POST/v1/workspaces/{name}/stopStop workspace
GET/v1/workspaces/{name}/logsGet container logs
GET/v1/workspaces/{name}/eventsGet workspace events
GET/v1/workspaces/{name}/podGet pod details
GET/v1/workspaces/{name}/metricsGet pod metrics
GET/v1/volumesList volumes (supports ?namespace= filter)
POST/v1/volumesCreate volume
DELETE/v1/volumes/{name}Delete volume
GET/v1/imagesList available images
GET/v1/namespacesList namespaces
GET/healthzHealth check
GET/openapi3.jsonOpenAPI 3.0 spec (JSON)
GET/proxy/{ns}/{name}/{path...}Reverse proxy to workspace web UI
GET/auth/configPublic auth configuration
GET/auth/loginInitiate OIDC login
GET/auth/callbackOIDC callback
POST/auth/logoutClear session
GET/auth/meCurrent user info
GET/admin/usersList users (admin)
POST/admin/usersCreate user (admin)
PUT/admin/users/{name}Update user (admin)
DELETE/admin/users/{name}Delete user (admin)
GET/admin/auth-configGet AuthConfig (admin)
PUT/admin/auth-configUpdate AuthConfig (admin)
GET/admin/crds/definitionsList CRD definitions
GET/admin/crds/workspacesList raw workspace CRs

UI Pages

RouteDescription
/Dashboard with summary cards and workspace list
/loginSSO login page (shown when auth enabled)
/workspacesWorkspace table with status, actions
/workspaces/newCreate workspace form
/workspaces/{name}Workspace detail (Overview, Logs, Events, Metrics, YAML)
/volumesVolume list
/volumes/newCreate volume form
/imagesAvailable images catalog
/adminAdmin index (visible to admins only when auth enabled)
/admin/usersUser management (list, create, enable/disable, delete)
/admin/settingsAuth settings (OIDC config, namespaces, registration)
/admin/apiAPI documentation (Scalar)
/admin/imagesImage CR editor
/admin/crdsCRD browser

LLM Deployment Prompt

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 to the current kubectl context

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-context and stop and ask me if it is not what I expect.

  1. Clone https://github.com/kube-workspaces/deploy.git and work from the repo root.
  2. 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 KiB last-applied-configuration annotation limit, so plain kubectl apply -f fails.
  3. Install the components: kubectl apply --server-side -k kustomize/base/. The manifests already point at the published ghcr.io/kube-workspaces/* images, so do not build any images.
  4. Install the workspace image catalog: make install-images. This applies the cluster-scoped Image CRs from images.yaml. Skipping this leaves the UI catalog empty — kustomize/base does not create any Image CRs.

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 (expect kube-workspaces-controller, -api, -proxy, -frontend)
  • No container has restarted. Every pod must show 0 restarts and no CrashLoopBackOff: 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 & then curl -fsS http://localhost:8888/healthz must return {"status":"ok"}.
  • curl -fsS http://localhost:8888/v1/images lists the catalog entries you applied in step 4.
  • The frontend serves HTML: background-forward svc/kube-workspaces-frontend 3000:80 and check curl -fsS http://localhost:3000/ returns HTTP 200 with an HTML body. The frontend has no /healthz endpoint — / is its probe path.

If any deployment fails to become Available, diagnose before continuing: kubectl describe pod on the not-ready pod, kubectl logs for its containers, and kubectl 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 to a local kind cluster

Deploy kube-workspaces to a local kind cluster.

  1. kind create cluster --name kube-workspaces
  2. Clone https://github.com/kube-workspaces/deploy.git, then from the repo root run make install-crd && make deploy-kustomize && make install-images.
  3. Wait for readiness: kubectl wait --for=condition=Available deployment --all -n kube-workspaces-system --timeout=300s

Verify 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/healthz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-proxy 8891:80 &curl -fsS http://localhost:8891/readyz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-frontend 3000:80 &curl -fsS http://localhost:3000/ returns HTTP 200 and HTML

Kill every port-forward you started when done. Note that the Ingress in kustomize/base hardcodes ingressClassName: traefik and 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).

Verify an end-to-end workspace

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.

  1. Apply this Workspace CR. Note that a raw Workspace does not need a matching Image CR — Image CRs only populate the UI/API catalog and supply defaults at creation time through the API. Use traefik/whoami rather than a heavyweight IDE image so the pull is a few MB and the check is fast:

    apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
    name: smoke-testnamespace: workspacesspec:
    template:
    spec:
    containers:
    - name: whoamiimage: traefik/whoamiports:
    - containerPort: 80name: workspace-port

    The workspaces namespace already exists — kustomize/base creates it.

  2. 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 over kubectl 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 port
    • kubectl get workspace smoke-test -n workspaces -o yaml and confirm status.readyReplicas is 1 and status.conditions reports ready
  3. Assert 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.

  4. 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.

  5. 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 the kubeworkspaces.io/stopped annotation
    • curl -fsS -X POST "http://localhost:8888/v1/workspaces/smoke-test/start?namespace=workspaces" → replicas returns to 1 and the pod becomes Ready again
  6. Clean 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.

Documentation

DocumentCovers
docs/architecture.svgComponent diagram
docs/authentication.mdOIDC setup, roles, personal namespaces
docs/domains.mdCustom hostnames and ingress routing
docs/proxy.mdHow workspace traffic is proxied
docs/security.mdServiceAccount tokens, RBAC, what is deliberately not hardened
docs/testing.mdThe test suite and how to run it
docs/releasing.mdRelease procedure across the five repositories
CONTRIBUTING.mdDevelopment setup

License

Apache License 2.0

About

Deployment manifests (Helm, Kustomize, ArgoCD) and documentation for kube-workspaces

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Kube Workspaces

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.

GitHub ReleaseLicensecontrollerapiproxyfrontendController CIAPI CIProxy CIFrontend CI

Architecture

Architecture diagram

Components

ComponentPathDescription
Controllercontroller/Kubernetes controller (kubebuilder) that reconciles Workspace, User, and AuthConfig CRs
APIapi/REST API service (Goa framework) providing workspace CRUD, volumes, images, auth, and a reverse proxy for workspace web UIs
Frontendfrontend/Next.js web UI with dashboard, workspace management, user management, namespace filtering, dark mode
Deploydeploy/Helm chart, Kustomize manifests, and ArgoCD Application for deployment

Features

  • 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

Quick Start

Prerequisites

  • 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.

Local cluster (kind)

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-frontend

Open 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.

Deploy to a 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.

Quick deploy with ArgoCD:

kubectl apply -f argocd/application-crds.yaml
kubectl apply -f argocd/application.yaml

Apply 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.

Quick deploy with Helm:

helm install kube-workspaces helm/kube-workspaces/ \
--namespace kube-workspaces-system --create-namespace

Or 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-namespace

Installing 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=false

Without 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.

Quick deploy with Kustomize:

kubectl apply --server-side -k kustomize/crds/
kubectl apply --server-side -k kustomize/base/

Docker Images

Released images are published to GHCR and are what the manifests reference by default — you do not need to build anything to deploy:

ComponentImage
controllerghcr.io/kube-workspaces/controller
apighcr.io/kube-workspaces/api
proxyghcr.io/kube-workspaces/proxy
frontendghcr.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):

forcin controller api proxy frontend;do
docker build -t "kube-workspaces-$c:dev""../$c"done

For 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

Authentication is opt-in and disabled by default. When disabled, the system operates without login — all users have full access (preserving backward compatibility).

Enabling Auth

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/v1alpha1kind: AuthConfigmetadata:
name: defaultspec:
enabled: trueoidc:
issuerURL: https://accounts.google.comclientID: <YOUR-GOOGLE-CLIENT-ID>clientSecret:
name: kube-workspaces-oidc-secretkey: client-secretsession:
signingKey:
name: kube-workspaces-session-secretkey: signing-keypersonalNamespaces:
enabled: truetemplate: "{{username}}"registration:
autoProvision: truedefaultRole: editoradminEmails:
- your-email@gmail.com

Create 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

Supported Identity Providers

  • Dex (recommended for multi-provider support) — supports LDAP, SAML, GitHub, GitLab, etc.
  • Okta — direct OIDC integration
  • Auth0 — direct OIDC integration
  • Any OIDC-compliant provider

User Management

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/v1alpha1kind: Usermetadata:
name: jane-doespec:
email: jane@example.comdisplayName: "Jane Doe"role: editornamespaceAccess:
- namespace: team-platformrole: editor

Roles

RolePermissions
adminFull access to all namespaces, user management, settings
editorCreate/edit/delete workspaces in assigned namespaces
viewerRead-only access to assigned namespaces

Personal 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 RoleBinding granting the user editor access
  • An optional ResourceQuota (if configured in AuthConfig)

CRDs

Workspace

The Workspace custom resource wraps a full Kubernetes PodSpec, giving complete flexibility over container configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
name: my-workspacenamespace: workspacesspec:
template:
spec:
containers:
- name: code-serverimage: codercom/code-server:latestargs: ["--bind-addr", "0.0.0.0:8080", "--auth", "none"]ports:
- containerPort: 8080resources:
requests:
cpu: "500m"memory: "512Mi"limits:
cpu: "2"memory: "2Gi"

Start/Stop

Workspaces are stopped by adding the annotation kubeworkspaces.io/stopped: "true", which sets the StatefulSet replicas to 0. Removing the annotation starts the workspace.

Image

Cluster-scoped CRD defining available workspace images with default configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Imagemetadata:
name: code-serverspec:
image: codercom/code-server:latestdisplayName: "Code Server (VS Code)"defaultPort: 8080icon: vscode

Available Images

The 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-images applies the full vendored images.yaml (currently 38 images). kustomize/base does not create any Image CRs on its own — this is a required separate step.

  • Helm: installs a curated set of 5 example images by default (installExampleImages: true). Set installCatalogImages: true to install the full catalog instead, or installExampleImages: false to install neither. Add your own images via the images: 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

Workspace Proxy

The API includes a built-in reverse proxy at /proxy/{namespace}/{name}/{path...} that provides direct browser access to running workspace web UIs.

Features

  • WebSocket support: Full WebSocket passthrough (needed for noVNC's websockify and 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.js or absolute paths) are caught via the Referer header 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

Local Access

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

API Endpoints

MethodPathDescription
GET/v1/workspacesList workspaces (supports ?namespace= filter)
GET/v1/workspaces/{name}Get workspace
POST/v1/workspacesCreate workspace
PUT/v1/workspaces/{name}Update workspace
DELETE/v1/workspaces/{name}Delete workspace
POST/v1/workspaces/{name}/startStart workspace
POST/v1/workspaces/{name}/stopStop workspace
GET/v1/workspaces/{name}/logsGet container logs
GET/v1/workspaces/{name}/eventsGet workspace events
GET/v1/workspaces/{name}/podGet pod details
GET/v1/workspaces/{name}/metricsGet pod metrics
GET/v1/volumesList volumes (supports ?namespace= filter)
POST/v1/volumesCreate volume
DELETE/v1/volumes/{name}Delete volume
GET/v1/imagesList available images
GET/v1/namespacesList namespaces
GET/healthzHealth check
GET/openapi3.jsonOpenAPI 3.0 spec (JSON)
GET/proxy/{ns}/{name}/{path...}Reverse proxy to workspace web UI
GET/auth/configPublic auth configuration
GET/auth/loginInitiate OIDC login
GET/auth/callbackOIDC callback
POST/auth/logoutClear session
GET/auth/meCurrent user info
GET/admin/usersList users (admin)
POST/admin/usersCreate user (admin)
PUT/admin/users/{name}Update user (admin)
DELETE/admin/users/{name}Delete user (admin)
GET/admin/auth-configGet AuthConfig (admin)
PUT/admin/auth-configUpdate AuthConfig (admin)
GET/admin/crds/definitionsList CRD definitions
GET/admin/crds/workspacesList raw workspace CRs

UI Pages

RouteDescription
/Dashboard with summary cards and workspace list
/loginSSO login page (shown when auth enabled)
/workspacesWorkspace table with status, actions
/workspaces/newCreate workspace form
/workspaces/{name}Workspace detail (Overview, Logs, Events, Metrics, YAML)
/volumesVolume list
/volumes/newCreate volume form
/imagesAvailable images catalog
/adminAdmin index (visible to admins only when auth enabled)
/admin/usersUser management (list, create, enable/disable, delete)
/admin/settingsAuth settings (OIDC config, namespaces, registration)
/admin/apiAPI documentation (Scalar)
/admin/imagesImage CR editor
/admin/crdsCRD browser

LLM Deployment Prompt

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 to the current kubectl context

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-context and stop and ask me if it is not what I expect.

  1. Clone https://github.com/kube-workspaces/deploy.git and work from the repo root.
  2. 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 KiB last-applied-configuration annotation limit, so plain kubectl apply -f fails.
  3. Install the components: kubectl apply --server-side -k kustomize/base/. The manifests already point at the published ghcr.io/kube-workspaces/* images, so do not build any images.
  4. Install the workspace image catalog: make install-images. This applies the cluster-scoped Image CRs from images.yaml. Skipping this leaves the UI catalog empty — kustomize/base does not create any Image CRs.

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 (expect kube-workspaces-controller, -api, -proxy, -frontend)
  • No container has restarted. Every pod must show 0 restarts and no CrashLoopBackOff: 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 & then curl -fsS http://localhost:8888/healthz must return {"status":"ok"}.
  • curl -fsS http://localhost:8888/v1/images lists the catalog entries you applied in step 4.
  • The frontend serves HTML: background-forward svc/kube-workspaces-frontend 3000:80 and check curl -fsS http://localhost:3000/ returns HTTP 200 with an HTML body. The frontend has no /healthz endpoint — / is its probe path.

If any deployment fails to become Available, diagnose before continuing: kubectl describe pod on the not-ready pod, kubectl logs for its containers, and kubectl 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 to a local kind cluster

Deploy kube-workspaces to a local kind cluster.

  1. kind create cluster --name kube-workspaces
  2. Clone https://github.com/kube-workspaces/deploy.git, then from the repo root run make install-crd && make deploy-kustomize && make install-images.
  3. Wait for readiness: kubectl wait --for=condition=Available deployment --all -n kube-workspaces-system --timeout=300s

Verify 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/healthz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-proxy 8891:80 &curl -fsS http://localhost:8891/readyz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-frontend 3000:80 &curl -fsS http://localhost:3000/ returns HTTP 200 and HTML

Kill every port-forward you started when done. Note that the Ingress in kustomize/base hardcodes ingressClassName: traefik and 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).

Verify an end-to-end workspace

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.

  1. Apply this Workspace CR. Note that a raw Workspace does not need a matching Image CR — Image CRs only populate the UI/API catalog and supply defaults at creation time through the API. Use traefik/whoami rather than a heavyweight IDE image so the pull is a few MB and the check is fast:

    apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
    name: smoke-testnamespace: workspacesspec:
    template:
    spec:
    containers:
    - name: whoamiimage: traefik/whoamiports:
    - containerPort: 80name: workspace-port

    The workspaces namespace already exists — kustomize/base creates it.

  2. 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 over kubectl 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 port
    • kubectl get workspace smoke-test -n workspaces -o yaml and confirm status.readyReplicas is 1 and status.conditions reports ready
  3. Assert 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.

  4. 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.

  5. 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 the kubeworkspaces.io/stopped annotation
    • curl -fsS -X POST "http://localhost:8888/v1/workspaces/smoke-test/start?namespace=workspaces" → replicas returns to 1 and the pod becomes Ready again
  6. Clean 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.

Documentation

DocumentCovers
docs/architecture.svgComponent diagram
docs/authentication.mdOIDC setup, roles, personal namespaces
docs/domains.mdCustom hostnames and ingress routing
docs/proxy.mdHow workspace traffic is proxied
docs/security.mdServiceAccount tokens, RBAC, what is deliberately not hardened
docs/testing.mdThe test suite and how to run it
docs/releasing.mdRelease procedure across the five repositories
CONTRIBUTING.mdDevelopment setup

License

Apache License 2.0

About

Deployment manifests (Helm, Kustomize, ArgoCD) and documentation for kube-workspaces

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Kube Workspaces

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.

GitHub ReleaseLicensecontrollerapiproxyfrontendController CIAPI CIProxy CIFrontend CI

Architecture

Architecture diagram

Components

ComponentPathDescription
Controllercontroller/Kubernetes controller (kubebuilder) that reconciles Workspace, User, and AuthConfig CRs
APIapi/REST API service (Goa framework) providing workspace CRUD, volumes, images, auth, and a reverse proxy for workspace web UIs
Frontendfrontend/Next.js web UI with dashboard, workspace management, user management, namespace filtering, dark mode
Deploydeploy/Helm chart, Kustomize manifests, and ArgoCD Application for deployment

Features

  • 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

Quick Start

Prerequisites

  • 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.

Local cluster (kind)

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-frontend

Open 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.

Deploy to a 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.

Quick deploy with ArgoCD:

kubectl apply -f argocd/application-crds.yaml
kubectl apply -f argocd/application.yaml

Apply 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.

Quick deploy with Helm:

helm install kube-workspaces helm/kube-workspaces/ \
--namespace kube-workspaces-system --create-namespace

Or 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-namespace

Installing 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=false

Without 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.

Quick deploy with Kustomize:

kubectl apply --server-side -k kustomize/crds/
kubectl apply --server-side -k kustomize/base/

Docker Images

Released images are published to GHCR and are what the manifests reference by default — you do not need to build anything to deploy:

ComponentImage
controllerghcr.io/kube-workspaces/controller
apighcr.io/kube-workspaces/api
proxyghcr.io/kube-workspaces/proxy
frontendghcr.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):

forcin controller api proxy frontend;do
docker build -t "kube-workspaces-$c:dev""../$c"done

For 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

Authentication is opt-in and disabled by default. When disabled, the system operates without login — all users have full access (preserving backward compatibility).

Enabling Auth

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/v1alpha1kind: AuthConfigmetadata:
name: defaultspec:
enabled: trueoidc:
issuerURL: https://accounts.google.comclientID: <YOUR-GOOGLE-CLIENT-ID>clientSecret:
name: kube-workspaces-oidc-secretkey: client-secretsession:
signingKey:
name: kube-workspaces-session-secretkey: signing-keypersonalNamespaces:
enabled: truetemplate: "{{username}}"registration:
autoProvision: truedefaultRole: editoradminEmails:
- your-email@gmail.com

Create 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

Supported Identity Providers

  • Dex (recommended for multi-provider support) — supports LDAP, SAML, GitHub, GitLab, etc.
  • Okta — direct OIDC integration
  • Auth0 — direct OIDC integration
  • Any OIDC-compliant provider

User Management

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/v1alpha1kind: Usermetadata:
name: jane-doespec:
email: jane@example.comdisplayName: "Jane Doe"role: editornamespaceAccess:
- namespace: team-platformrole: editor

Roles

RolePermissions
adminFull access to all namespaces, user management, settings
editorCreate/edit/delete workspaces in assigned namespaces
viewerRead-only access to assigned namespaces

Personal 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 RoleBinding granting the user editor access
  • An optional ResourceQuota (if configured in AuthConfig)

CRDs

Workspace

The Workspace custom resource wraps a full Kubernetes PodSpec, giving complete flexibility over container configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
name: my-workspacenamespace: workspacesspec:
template:
spec:
containers:
- name: code-serverimage: codercom/code-server:latestargs: ["--bind-addr", "0.0.0.0:8080", "--auth", "none"]ports:
- containerPort: 8080resources:
requests:
cpu: "500m"memory: "512Mi"limits:
cpu: "2"memory: "2Gi"

Start/Stop

Workspaces are stopped by adding the annotation kubeworkspaces.io/stopped: "true", which sets the StatefulSet replicas to 0. Removing the annotation starts the workspace.

Image

Cluster-scoped CRD defining available workspace images with default configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Imagemetadata:
name: code-serverspec:
image: codercom/code-server:latestdisplayName: "Code Server (VS Code)"defaultPort: 8080icon: vscode

Available Images

The 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-images applies the full vendored images.yaml (currently 38 images). kustomize/base does not create any Image CRs on its own — this is a required separate step.

  • Helm: installs a curated set of 5 example images by default (installExampleImages: true). Set installCatalogImages: true to install the full catalog instead, or installExampleImages: false to install neither. Add your own images via the images: 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

Workspace Proxy

The API includes a built-in reverse proxy at /proxy/{namespace}/{name}/{path...} that provides direct browser access to running workspace web UIs.

Features

  • WebSocket support: Full WebSocket passthrough (needed for noVNC's websockify and 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.js or absolute paths) are caught via the Referer header 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

Local Access

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

API Endpoints

MethodPathDescription
GET/v1/workspacesList workspaces (supports ?namespace= filter)
GET/v1/workspaces/{name}Get workspace
POST/v1/workspacesCreate workspace
PUT/v1/workspaces/{name}Update workspace
DELETE/v1/workspaces/{name}Delete workspace
POST/v1/workspaces/{name}/startStart workspace
POST/v1/workspaces/{name}/stopStop workspace
GET/v1/workspaces/{name}/logsGet container logs
GET/v1/workspaces/{name}/eventsGet workspace events
GET/v1/workspaces/{name}/podGet pod details
GET/v1/workspaces/{name}/metricsGet pod metrics
GET/v1/volumesList volumes (supports ?namespace= filter)
POST/v1/volumesCreate volume
DELETE/v1/volumes/{name}Delete volume
GET/v1/imagesList available images
GET/v1/namespacesList namespaces
GET/healthzHealth check
GET/openapi3.jsonOpenAPI 3.0 spec (JSON)
GET/proxy/{ns}/{name}/{path...}Reverse proxy to workspace web UI
GET/auth/configPublic auth configuration
GET/auth/loginInitiate OIDC login
GET/auth/callbackOIDC callback
POST/auth/logoutClear session
GET/auth/meCurrent user info
GET/admin/usersList users (admin)
POST/admin/usersCreate user (admin)
PUT/admin/users/{name}Update user (admin)
DELETE/admin/users/{name}Delete user (admin)
GET/admin/auth-configGet AuthConfig (admin)
PUT/admin/auth-configUpdate AuthConfig (admin)
GET/admin/crds/definitionsList CRD definitions
GET/admin/crds/workspacesList raw workspace CRs

UI Pages

RouteDescription
/Dashboard with summary cards and workspace list
/loginSSO login page (shown when auth enabled)
/workspacesWorkspace table with status, actions
/workspaces/newCreate workspace form
/workspaces/{name}Workspace detail (Overview, Logs, Events, Metrics, YAML)
/volumesVolume list
/volumes/newCreate volume form
/imagesAvailable images catalog
/adminAdmin index (visible to admins only when auth enabled)
/admin/usersUser management (list, create, enable/disable, delete)
/admin/settingsAuth settings (OIDC config, namespaces, registration)
/admin/apiAPI documentation (Scalar)
/admin/imagesImage CR editor
/admin/crdsCRD browser

LLM Deployment Prompt

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 to the current kubectl context

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-context and stop and ask me if it is not what I expect.

  1. Clone https://github.com/kube-workspaces/deploy.git and work from the repo root.
  2. 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 KiB last-applied-configuration annotation limit, so plain kubectl apply -f fails.
  3. Install the components: kubectl apply --server-side -k kustomize/base/. The manifests already point at the published ghcr.io/kube-workspaces/* images, so do not build any images.
  4. Install the workspace image catalog: make install-images. This applies the cluster-scoped Image CRs from images.yaml. Skipping this leaves the UI catalog empty — kustomize/base does not create any Image CRs.

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 (expect kube-workspaces-controller, -api, -proxy, -frontend)
  • No container has restarted. Every pod must show 0 restarts and no CrashLoopBackOff: 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 & then curl -fsS http://localhost:8888/healthz must return {"status":"ok"}.
  • curl -fsS http://localhost:8888/v1/images lists the catalog entries you applied in step 4.
  • The frontend serves HTML: background-forward svc/kube-workspaces-frontend 3000:80 and check curl -fsS http://localhost:3000/ returns HTTP 200 with an HTML body. The frontend has no /healthz endpoint — / is its probe path.

If any deployment fails to become Available, diagnose before continuing: kubectl describe pod on the not-ready pod, kubectl logs for its containers, and kubectl 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 to a local kind cluster

Deploy kube-workspaces to a local kind cluster.

  1. kind create cluster --name kube-workspaces
  2. Clone https://github.com/kube-workspaces/deploy.git, then from the repo root run make install-crd && make deploy-kustomize && make install-images.
  3. Wait for readiness: kubectl wait --for=condition=Available deployment --all -n kube-workspaces-system --timeout=300s

Verify 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/healthz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-proxy 8891:80 &curl -fsS http://localhost:8891/readyz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-frontend 3000:80 &curl -fsS http://localhost:3000/ returns HTTP 200 and HTML

Kill every port-forward you started when done. Note that the Ingress in kustomize/base hardcodes ingressClassName: traefik and 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).

Verify an end-to-end workspace

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.

  1. Apply this Workspace CR. Note that a raw Workspace does not need a matching Image CR — Image CRs only populate the UI/API catalog and supply defaults at creation time through the API. Use traefik/whoami rather than a heavyweight IDE image so the pull is a few MB and the check is fast:

    apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
    name: smoke-testnamespace: workspacesspec:
    template:
    spec:
    containers:
    - name: whoamiimage: traefik/whoamiports:
    - containerPort: 80name: workspace-port

    The workspaces namespace already exists — kustomize/base creates it.

  2. 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 over kubectl 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 port
    • kubectl get workspace smoke-test -n workspaces -o yaml and confirm status.readyReplicas is 1 and status.conditions reports ready
  3. Assert 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.

  4. 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.

  5. 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 the kubeworkspaces.io/stopped annotation
    • curl -fsS -X POST "http://localhost:8888/v1/workspaces/smoke-test/start?namespace=workspaces" → replicas returns to 1 and the pod becomes Ready again
  6. Clean 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.

Documentation

DocumentCovers
docs/architecture.svgComponent diagram
docs/authentication.mdOIDC setup, roles, personal namespaces
docs/domains.mdCustom hostnames and ingress routing
docs/proxy.mdHow workspace traffic is proxied
docs/security.mdServiceAccount tokens, RBAC, what is deliberately not hardened
docs/testing.mdThe test suite and how to run it
docs/releasing.mdRelease procedure across the five repositories
CONTRIBUTING.mdDevelopment setup

License

Apache License 2.0

About

Deployment manifests (Helm, Kustomize, ArgoCD) and documentation for kube-workspaces

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Kube Workspaces

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.

GitHub ReleaseLicensecontrollerapiproxyfrontendController CIAPI CIProxy CIFrontend CI

Architecture

Architecture diagram

Components

ComponentPathDescription
Controllercontroller/Kubernetes controller (kubebuilder) that reconciles Workspace, User, and AuthConfig CRs
APIapi/REST API service (Goa framework) providing workspace CRUD, volumes, images, auth, and a reverse proxy for workspace web UIs
Frontendfrontend/Next.js web UI with dashboard, workspace management, user management, namespace filtering, dark mode
Deploydeploy/Helm chart, Kustomize manifests, and ArgoCD Application for deployment

Features

  • 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

Quick Start

Prerequisites

  • 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.

Local cluster (kind)

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-frontend

Open 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.

Deploy to a 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.

Quick deploy with ArgoCD:

kubectl apply -f argocd/application-crds.yaml
kubectl apply -f argocd/application.yaml

Apply 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.

Quick deploy with Helm:

helm install kube-workspaces helm/kube-workspaces/ \
--namespace kube-workspaces-system --create-namespace

Or 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-namespace

Installing 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=false

Without 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.

Quick deploy with Kustomize:

kubectl apply --server-side -k kustomize/crds/
kubectl apply --server-side -k kustomize/base/

Docker Images

Released images are published to GHCR and are what the manifests reference by default — you do not need to build anything to deploy:

ComponentImage
controllerghcr.io/kube-workspaces/controller
apighcr.io/kube-workspaces/api
proxyghcr.io/kube-workspaces/proxy
frontendghcr.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):

forcin controller api proxy frontend;do
docker build -t "kube-workspaces-$c:dev""../$c"done

For 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

Authentication is opt-in and disabled by default. When disabled, the system operates without login — all users have full access (preserving backward compatibility).

Enabling Auth

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/v1alpha1kind: AuthConfigmetadata:
name: defaultspec:
enabled: trueoidc:
issuerURL: https://accounts.google.comclientID: <YOUR-GOOGLE-CLIENT-ID>clientSecret:
name: kube-workspaces-oidc-secretkey: client-secretsession:
signingKey:
name: kube-workspaces-session-secretkey: signing-keypersonalNamespaces:
enabled: truetemplate: "{{username}}"registration:
autoProvision: truedefaultRole: editoradminEmails:
- your-email@gmail.com

Create 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

Supported Identity Providers

  • Dex (recommended for multi-provider support) — supports LDAP, SAML, GitHub, GitLab, etc.
  • Okta — direct OIDC integration
  • Auth0 — direct OIDC integration
  • Any OIDC-compliant provider

User Management

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/v1alpha1kind: Usermetadata:
name: jane-doespec:
email: jane@example.comdisplayName: "Jane Doe"role: editornamespaceAccess:
- namespace: team-platformrole: editor

Roles

RolePermissions
adminFull access to all namespaces, user management, settings
editorCreate/edit/delete workspaces in assigned namespaces
viewerRead-only access to assigned namespaces

Personal 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 RoleBinding granting the user editor access
  • An optional ResourceQuota (if configured in AuthConfig)

CRDs

Workspace

The Workspace custom resource wraps a full Kubernetes PodSpec, giving complete flexibility over container configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
name: my-workspacenamespace: workspacesspec:
template:
spec:
containers:
- name: code-serverimage: codercom/code-server:latestargs: ["--bind-addr", "0.0.0.0:8080", "--auth", "none"]ports:
- containerPort: 8080resources:
requests:
cpu: "500m"memory: "512Mi"limits:
cpu: "2"memory: "2Gi"

Start/Stop

Workspaces are stopped by adding the annotation kubeworkspaces.io/stopped: "true", which sets the StatefulSet replicas to 0. Removing the annotation starts the workspace.

Image

Cluster-scoped CRD defining available workspace images with default configuration:

apiVersion: kubeworkspaces.io/v1alpha1kind: Imagemetadata:
name: code-serverspec:
image: codercom/code-server:latestdisplayName: "Code Server (VS Code)"defaultPort: 8080icon: vscode

Available Images

The 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-images applies the full vendored images.yaml (currently 38 images). kustomize/base does not create any Image CRs on its own — this is a required separate step.

  • Helm: installs a curated set of 5 example images by default (installExampleImages: true). Set installCatalogImages: true to install the full catalog instead, or installExampleImages: false to install neither. Add your own images via the images: 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

Workspace Proxy

The API includes a built-in reverse proxy at /proxy/{namespace}/{name}/{path...} that provides direct browser access to running workspace web UIs.

Features

  • WebSocket support: Full WebSocket passthrough (needed for noVNC's websockify and 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.js or absolute paths) are caught via the Referer header 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

Local Access

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

API Endpoints

MethodPathDescription
GET/v1/workspacesList workspaces (supports ?namespace= filter)
GET/v1/workspaces/{name}Get workspace
POST/v1/workspacesCreate workspace
PUT/v1/workspaces/{name}Update workspace
DELETE/v1/workspaces/{name}Delete workspace
POST/v1/workspaces/{name}/startStart workspace
POST/v1/workspaces/{name}/stopStop workspace
GET/v1/workspaces/{name}/logsGet container logs
GET/v1/workspaces/{name}/eventsGet workspace events
GET/v1/workspaces/{name}/podGet pod details
GET/v1/workspaces/{name}/metricsGet pod metrics
GET/v1/volumesList volumes (supports ?namespace= filter)
POST/v1/volumesCreate volume
DELETE/v1/volumes/{name}Delete volume
GET/v1/imagesList available images
GET/v1/namespacesList namespaces
GET/healthzHealth check
GET/openapi3.jsonOpenAPI 3.0 spec (JSON)
GET/proxy/{ns}/{name}/{path...}Reverse proxy to workspace web UI
GET/auth/configPublic auth configuration
GET/auth/loginInitiate OIDC login
GET/auth/callbackOIDC callback
POST/auth/logoutClear session
GET/auth/meCurrent user info
GET/admin/usersList users (admin)
POST/admin/usersCreate user (admin)
PUT/admin/users/{name}Update user (admin)
DELETE/admin/users/{name}Delete user (admin)
GET/admin/auth-configGet AuthConfig (admin)
PUT/admin/auth-configUpdate AuthConfig (admin)
GET/admin/crds/definitionsList CRD definitions
GET/admin/crds/workspacesList raw workspace CRs

UI Pages

RouteDescription
/Dashboard with summary cards and workspace list
/loginSSO login page (shown when auth enabled)
/workspacesWorkspace table with status, actions
/workspaces/newCreate workspace form
/workspaces/{name}Workspace detail (Overview, Logs, Events, Metrics, YAML)
/volumesVolume list
/volumes/newCreate volume form
/imagesAvailable images catalog
/adminAdmin index (visible to admins only when auth enabled)
/admin/usersUser management (list, create, enable/disable, delete)
/admin/settingsAuth settings (OIDC config, namespaces, registration)
/admin/apiAPI documentation (Scalar)
/admin/imagesImage CR editor
/admin/crdsCRD browser

LLM Deployment Prompt

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 to the current kubectl context

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-context and stop and ask me if it is not what I expect.

  1. Clone https://github.com/kube-workspaces/deploy.git and work from the repo root.
  2. 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 KiB last-applied-configuration annotation limit, so plain kubectl apply -f fails.
  3. Install the components: kubectl apply --server-side -k kustomize/base/. The manifests already point at the published ghcr.io/kube-workspaces/* images, so do not build any images.
  4. Install the workspace image catalog: make install-images. This applies the cluster-scoped Image CRs from images.yaml. Skipping this leaves the UI catalog empty — kustomize/base does not create any Image CRs.

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 (expect kube-workspaces-controller, -api, -proxy, -frontend)
  • No container has restarted. Every pod must show 0 restarts and no CrashLoopBackOff: 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 & then curl -fsS http://localhost:8888/healthz must return {"status":"ok"}.
  • curl -fsS http://localhost:8888/v1/images lists the catalog entries you applied in step 4.
  • The frontend serves HTML: background-forward svc/kube-workspaces-frontend 3000:80 and check curl -fsS http://localhost:3000/ returns HTTP 200 with an HTML body. The frontend has no /healthz endpoint — / is its probe path.

If any deployment fails to become Available, diagnose before continuing: kubectl describe pod on the not-ready pod, kubectl logs for its containers, and kubectl 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 to a local kind cluster

Deploy kube-workspaces to a local kind cluster.

  1. kind create cluster --name kube-workspaces
  2. Clone https://github.com/kube-workspaces/deploy.git, then from the repo root run make install-crd && make deploy-kustomize && make install-images.
  3. Wait for readiness: kubectl wait --for=condition=Available deployment --all -n kube-workspaces-system --timeout=300s

Verify 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/healthz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-proxy 8891:80 &curl -fsS http://localhost:8891/readyz returns {"status":"ok"}
  • kubectl port-forward -n kube-workspaces-system svc/kube-workspaces-frontend 3000:80 &curl -fsS http://localhost:3000/ returns HTTP 200 and HTML

Kill every port-forward you started when done. Note that the Ingress in kustomize/base hardcodes ingressClassName: traefik and 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).

Verify an end-to-end workspace

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.

  1. Apply this Workspace CR. Note that a raw Workspace does not need a matching Image CR — Image CRs only populate the UI/API catalog and supply defaults at creation time through the API. Use traefik/whoami rather than a heavyweight IDE image so the pull is a few MB and the check is fast:

    apiVersion: kubeworkspaces.io/v1alpha1kind: Workspacemetadata:
    name: smoke-testnamespace: workspacesspec:
    template:
    spec:
    containers:
    - name: whoamiimage: traefik/whoamiports:
    - containerPort: 80name: workspace-port

    The workspaces namespace already exists — kustomize/base creates it.

  2. 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 over kubectl 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 port
    • kubectl get workspace smoke-test -n workspaces -o yaml and confirm status.readyReplicas is 1 and status.conditions reports ready
  3. Assert 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.

  4. 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.

  5. 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 the kubeworkspaces.io/stopped annotation
    • curl -fsS -X POST "http://localhost:8888/v1/workspaces/smoke-test/start?namespace=workspaces" → replicas returns to 1 and the pod becomes Ready again
  6. Clean 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.

Documentation

DocumentCovers
docs/architecture.svgComponent diagram
docs/authentication.mdOIDC setup, roles, personal namespaces
docs/domains.mdCustom hostnames and ingress routing
docs/proxy.mdHow workspace traffic is proxied
docs/security.mdServiceAccount tokens, RBAC, what is deliberately not hardened
docs/testing.mdThe test suite and how to run it
docs/releasing.mdRelease procedure across the five repositories
CONTRIBUTING.mdDevelopment setup

License

Apache License 2.0

About

Deployment manifests (Helm, Kustomize, ArgoCD) and documentation for kube-workspaces

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages