A containerized cloud development environment with web-based GUI access, featuring:
- VSCode or Google Antigravity via browser-based VNC (port 5800)
- SSH access option (OpenSSH on port 22, additive with any IDE)
- Claude Code, OpenCode, and Crush AI coding agents (terminal-based)
- Built-in web file manager for uploading/downloading files via the VNC web interface
- Helm CLI included for Kubernetes chart development and deployment
- Automatic GitHub repo cloning on startup
- Persistent home directory via ReadWriteMany PVC
- Kubernetes-native Helm chart deployment
# Add the Helm repository
helm repo add devcontainer https://cpfarhood.github.io/devcontainer
helm repo update
# Deploy with one command
helm install mydev devcontainer/devcontainer \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo# Clone and customize the quickstart template
cp chart/values-quickstart.yaml my-values.yaml
# Edit my-values.yaml to set your name and repository
helm install mydev ./chart -f my-values.yamlhelm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepoThe secret is picked up automatically via envFrom. Keys recognised:
| Key | Purpose |
|---|---|
GITHUB_TOKEN |
PAT for private repo access (repo scope) |
VNC_PASSWORD |
Password for the VNC web UI |
ANTHROPIC_API_KEY |
API key — alternative to browser-based Claude login |
SSH_AUTHORIZED_KEYS |
Public key(s) for SSH access (required when ssh: true) |
HOMEASSISTANT_URL |
Home Assistant URL (required when mcp.sidecars.homeassistant.enabled: true) |
HOMEASSISTANT_TOKEN |
Home Assistant long-lived access token (required when mcp.sidecars.homeassistant.enabled: true) |
DATABASE_URI |
PostgreSQL connection string (required when mcp.sidecars.pgtuner.enabled: true) |
PGTUNER_EXCLUDE_USERIDS |
Comma-separated PostgreSQL user OIDs to exclude from monitoring (optional) |
kubectl create secret generic devcontainer-mydev-secrets-env \
--from-literal=GITHUB_TOKEN='ghp_...' \
--from-literal=VNC_PASSWORD='changeme'Or use SealedSecrets:
kubectl create secret generic devcontainer-mydev-secrets-env \
--from-literal=GITHUB_TOKEN='ghp_...' \
--from-literal=VNC_PASSWORD='changeme' \
--dry-run=client -o yaml | \
kubeseal --format=yaml | kubectl apply -f -helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo# Local port-forward
kubectl port-forward deployment/devcontainer-mydev 5800:5800
open http://localhost:5800Or configure an ingress / Gateway API HTTPRoute pointing at port 5800.
On first launch, open a terminal in the VSCode GUI and run:
claudeA Chrome browser window will open inside VNC for the Claude Max OAuth login. Credentials are stored on the home PVC and persist across pod restarts.
The Helm chart uses a logical organization with these main sections:
- Basic Configuration: name, image, githubRepo
- Access & Interface: IDE, SSH, display, user settings
- Infrastructure: storage, resources, cluster access
- Integrations: MCP sidecars
- Smart Defaults: auto-detection and profiles
📖 Documentation:
- USAGE.md - Comprehensive examples and scenarios
- values-quickstart.yaml - Minimal configuration
- values.schema.json - IDE validation support
| Value | Default | Description |
|---|---|---|
name |
"" |
Instance name — used in all resource names (devcontainer-{name}) |
githubRepo |
"" |
Repository to clone into /workspace on startup |
ide.type |
vscode |
IDE to launch — vscode, antigravity, or none (see below) |
ssh.enabled |
false |
Also start an OpenSSH server on port 22 (additive, any IDE) |
fileManager.enabled |
false |
Enable the built-in web file manager for upload/download |
image.repository |
ghcr.io/cpfarhood/devcontainer |
Container image |
image.tag |
latest |
Image tag |
ide.type controls what GUI is launched in the VNC session:
| Value | Port | Description |
|---|---|---|
vscode (default) |
5800 (VNC) | VSCode desktop via browser-based VNC |
antigravity |
5800 (VNC) | Google Antigravity (VSCode fork with AI) via VNC |
none |
— | No IDE; container stays alive (useful when ssh: true) |
ssh.enabled: true starts OpenSSH on port 22 in addition to the IDE. It works with any ide.type value:
# SSH-only (no VNC)
helm install mydev ./chart --set name=mydev --set ide.type=none --set ssh.enabled=true
# VSCode in VNC + SSH access at the same time
helm install mydev ./chart --set name=mydev --set ssh.enabled=trueAdd your public key to the env secret:
kubectl create secret generic devcontainer-mydev-secrets-env \
--from-literal=GITHUB_TOKEN='ghp_...' \
--from-literal=SSH_AUTHORIZED_KEYS='ssh-ed25519 AAAA...'Then connect:
kubectl port-forward deployment/devcontainer-mydev 2222:22
ssh -p 2222 user@localhostThe base image includes a built-in web file manager for uploading and downloading files through the VNC web interface (port 5800). No additional sidecar is needed.
| Value | Default | Description |
|---|---|---|
fileManager.enabled |
false |
Enable the web file manager |
fileManager.allowedPaths |
/workspace,/config |
Paths accessible by the file manager (AUTO, ALL, or comma-separated) |
fileManager.deniedPaths |
"" |
Paths to deny (takes precedence over allowed) |
# Enable the file manager
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set fileManager.enabled=trueThe clusterAccess value provisions a ServiceAccount, Role/ClusterRole, and binding so the devcontainer pod can interact with the Kubernetes API. The default is none — no RBAC resources are created.
| Value | Scope | Verbs |
|---|---|---|
none (default) |
— | no access |
readonlyns |
release namespace | get, list, watch |
readwritens |
release namespace | * |
readonly |
cluster-wide | get, list, watch |
readwrite |
cluster-wide | * |
# Give the pod read-only access to its own namespace
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set clusterAccess=readonlynsWith any non-none value, a ServiceAccount named devcontainer-{name} is created and set as the pod's serviceAccountName, so kubectl and any in-cluster API calls use it automatically.
The devcontainer includes MCP (Model Context Protocol) servers as sidecar containers that enable AI assistants to interact with various services:
| Sidecar | Default | Purpose |
|---|---|---|
mcp.sidecars.kubernetes.enabled |
true |
Kubernetes API access via MCP |
mcp.sidecars.flux.enabled |
true |
Flux GitOps operations via MCP |
mcp.sidecars.homeassistant.enabled |
false |
Home Assistant smart home control via MCP |
mcp.sidecars.pgtuner.enabled |
false |
PostgreSQL performance tuning and analysis via MCP |
mcp.sidecars.playwright.enabled |
true |
Browser automation and web testing via MCP |
Notes:
- GitHub MCP is accessed via the Copilot API (
https://api.githubcopilot.com/mcp/), not as a sidecar - Kubernetes and Flux sidecars require
clusterAccess!=noneto be deployed (automatically disabled when no cluster access) - Kubernetes and Flux sidecars inherit the pod's ServiceAccount RBAC permissions (controlled by
clusterAccess) - Home Assistant sidecar requires
HOMEASSISTANT_URLandHOMEASSISTANT_TOKENin the env secret - PostgreSQL tuner sidecar requires
DATABASE_URIin the env secret (PostgreSQL connection string) - Playwright sidecar provides browser automation and web testing capabilities
Disable MCP sidecars:
# Disable multiple sidecars
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set mcp.sidecars.kubernetes.enabled=false \
--set mcp.sidecars.flux.enabled=false \
--set mcp.sidecars.playwright.enabled=false
# Or selectively disable
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set mcp.sidecars.flux.enabled=false # Disable only Flux MCPEnable Home Assistant MCP:
# Create secret with Home Assistant credentials
kubectl create secret generic devcontainer-mydev-secrets-env \
--from-literal=GITHUB_TOKEN='ghp_...' \
--from-literal=HOMEASSISTANT_URL='http://homeassistant.local:8123' \
--from-literal=HOMEASSISTANT_TOKEN='your_long_lived_access_token'
# Deploy with Home Assistant MCP enabled
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set mcp.sidecars.homeassistant.enabled=trueEnable PostgreSQL Tuner MCP:
# Create secret with PostgreSQL connection string
kubectl create secret generic devcontainer-mydev-secrets-env \
--from-literal=GITHUB_TOKEN='ghp_...' \
--from-literal=DATABASE_URI='postgresql://user:password@postgres.example.com:5432/dbname'
# Deploy with PostgreSQL tuner MCP enabled
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set mcp.sidecars.pgtuner.enabled=trueCustom MCP configuration:
# values.yaml override
mcp:
sidecars:
kubernetes:
enabled: true
image:
repository: quay.io/containers/kubernetes_mcp_server
tag: v0.0.57
port: 8080
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "500m"
flux:
enabled: false # Disabled in this example
homeassistant:
enabled: true
image:
repository: ghcr.io/homeassistant-ai/ha-mcp
tag: stable
port: 8087
pgtuner:
enabled: true
image:
repository: dog830228/pgtuner_mcp
tag: latest
port: 8085
playwright:
enabled: true
image:
repository: mcr.microsoft.com/playwright/mcp
tag: latest
port: 8086
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "1000m"| Value | Default | Description |
|---|---|---|
display.width |
1920 |
VNC width (px) |
display.height |
1080 |
VNC height (px) |
display.secureConnection |
0 |
Set to 1 if TLS is not terminated upstream |
user.id |
1000 |
UID for the app user |
user.groupId |
1000 |
GID for the app user |
storage.size |
32Gi |
Home PVC size |
storage.className |
ceph-filesystem |
StorageClass (must be ReadWriteMany) |
shm.sizeLimit |
2Gi |
/dev/shm size (memory-backed; used by Electron apps) |
resources.requests.memory |
2Gi |
|
resources.requests.cpu |
1000m |
|
resources.limits.memory |
8Gi |
|
resources.limits.cpu |
4000m |
|
envSecretName |
devcontainer-{name}-secrets-env |
Override the secret name |
Container start
→ cont-init.d/20-fix-user-shell.sh — fix shell/home on baseimage-gui app user
→ cont-init.d/25-start-sshd.sh — start sshd if SSH=true
→ /startapp.sh (runs as app user, UID 1000)
→ init-repo.sh
→ clone / pull GITHUB_REPO into /workspace/{repo}
→ IDE=vscode: code --new-window --wait /workspace/{repo}
IDE=antigravity: antigravity --no-sandbox --user-data-dir ~/.config/antigravity ... /workspace/{repo}
IDE=none: sleep infinity
(SSH=true: sshd also running as root on port 22; host keys persisted on PVC)
| Mount | Source | Persistence |
|---|---|---|
/config |
ReadWriteMany PVC (userhome-{name}) |
Survives pod restarts — stores Claude credentials, dotfiles, git config |
/workspace |
emptyDir |
Ephemeral — repo is re-cloned on each pod start |
Browser-based OAuth login is the primary method (works inside VNC via the Chrome wrapper). If you prefer API key auth:
kubectl patch secret devcontainer-mydev-secrets-env \
--type='json' \
-p='[{"op":"add","path":"/data/ANTHROPIC_API_KEY","value":"'$(echo -n "sk-ant-..." | base64)'"}]'Then restart the pod to pick up the new env var.
kubectl port-forward deployment/devcontainer-mydev 5800:5800
kubectl logs deployment/devcontainer-mydev
kubectl describe pod -l app.kubernetes.io/instance=mydevThe chart uses image.tag: latest. Kubernetes won't restart the pod on a Helm upgrade unless the Deployment spec changes. Force a restart manually:
kubectl rollout restart deployment/devcontainer-mydevkubectl logs deployment/devcontainer-mydev | grep "Repository Initialization"
kubectl exec deployment/devcontainer-mydev -- env | grep GITHUBdocker run -d \
-p 5800:5800 \
-e GITHUB_REPO="https://github.com/youruser/yourrepo" \
-e GITHUB_TOKEN="ghp_..." \
-e VNC_PASSWORD="changeme" \
-v $(pwd)/home:/home \
ghcr.io/cpfarhood/devcontainer:latestdocker build -t ghcr.io/cpfarhood/devcontainer:latest .
docker push ghcr.io/cpfarhood/devcontainer:latestThe image is also built and pushed automatically by CI on every push to main and on version tags (v*).
- Base image: jlesage/docker-baseimage-gui
- AI assistant: Claude