This commit addresses multiple GitHub issues and adds significant enhancements: 🔧 **Issue #8 - Browser Window Title Fix** - Updated browser window title from "Antigravity Dev Container" to "Dev Container" - Changed APP_NAME in Dockerfile and startup script for consistency 🚀 **Issue #30 - PostgreSQL Tuner MCP Sidecar** - Added PostgreSQL performance tuning MCP sidecar (dog830228/pgtuner_mcp) - Provides AI assistant with database analysis capabilities: - Slow query analysis and optimization suggestions - Index recommendations with HypoPG virtual testing - Table and index bloat detection - Vacuum operation tracking and health scoring - Requires DATABASE_URI in env secret, optional PGTUNER_EXCLUDE_USERIDS - Disabled by default, configurable via mcpSidecars.pgtuner.enabled - Updated CLAUDE.md documentation with full configuration examples 🎭 **Playwright: Centralized Service → Sidecar Conversion** - Converted Playwright from external service to self-contained sidecar - Updated .mcp.json endpoint: cluster service → http://localhost:8086/sse - Added deployment configuration with proper health checks - Enabled by default for immediate browser automation capabilities - Higher resource allocation (512Mi memory, 1 CPU) for browser workloads 📚 **Documentation Updates** - Updated README.md: "Antigravity Dev Container" → "Dev Container" - Added comprehensive MCP sidecars documentation - Updated secret keys table with database-uri and pgtuner-exclude-userids - Added configuration examples for all 6 MCP sidecars: - kubernetes-mcp (enabled) - flux-mcp (enabled) - github-mcp (disabled - archived) - homeassistant-mcp (disabled - needs secrets) - pgtuner-mcp (disabled - needs DATABASE_URI) - playwright-mcp (enabled - browser automation) - Updated CLAUDE.md with complete sidecar reference table - Added Helm deployment examples and troubleshooting 🏗️ **Architecture Improvements** - All MCP sidecars now self-contained within pod - Consistent SSE transport configuration across all sidecars - Proper health checks and resource limits for all services - Simplified deployment with no external service dependencies Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Dev Container
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)
- Happy Coder AI assistant backed by Claude
- Automatic GitHub repo cloning on startup
- Persistent home directory via ReadWriteMany PVC
- Kubernetes-native Helm chart deployment
Quick Start
1. Create a secret
The 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 mcpSidecars.homeassistant.enabled: true) |
homeassistant-token |
Home Assistant long-lived access token (required when mcpSidecars.homeassistant.enabled: true) |
database-uri |
PostgreSQL connection string (required when mcpSidecars.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 -
2. Deploy with Helm
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo
3. Access
# Local port-forward
kubectl port-forward deployment/devcontainer-mydev 5800:5800
open http://localhost:5800
Or configure an ingress / Gateway API HTTPRoute pointing at port 5800.
4. Authenticate Claude
On first launch, open a terminal in the VSCode GUI and run:
claude
A 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.
Helm Chart Reference
Core values
| Value | Default | Description |
|---|---|---|
name |
"" |
Instance name — used in all resource names (devcontainer-{name}) |
githubRepo |
"" |
Repository to clone into /workspace on startup |
ide |
vscode |
IDE to launch — vscode, antigravity, or none (see below) |
ssh |
false |
Also start an OpenSSH server on port 22 (additive, any ide) |
image.repository |
ghcr.io/cpfarhood/devcontainer |
Container image |
image.tag |
latest |
Image tag |
IDE choice
ide 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 access
ssh: true starts OpenSSH on port 22 in addition to the IDE. It works with any ide value:
# SSH-only (no VNC)
helm install mydev ./chart --set name=mydev --set ide=none --set ssh=true
# VSCode in VNC + SSH access at the same time
helm install mydev ./chart --set name=mydev --set ssh=true
Add 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@localhost
Happy Coder
| Value | Default | Description |
|---|---|---|
happyServerUrl |
https://happy.farh.net |
Happy Coder server endpoint |
happyWebappUrl |
https://happy-coder.farh.net |
Happy Coder webapp URL |
happyHomeDir |
/home/user/.happy |
Happy runtime state directory (persists on the home PVC) |
happyExperimental |
true |
Enable experimental Happy features |
Kubernetes cluster access
The 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=readonlyns
With 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.
MCP Sidecars
The devcontainer includes MCP (Model Context Protocol) servers as sidecar containers that enable AI assistants to interact with various services:
| Sidecar | Default | Purpose |
|---|---|---|
mcpSidecars.kubernetes.enabled |
true |
Kubernetes API access via MCP |
mcpSidecars.flux.enabled |
true |
Flux GitOps operations via MCP |
mcpSidecars.github.enabled |
false |
GitHub API access via MCP (DISABLED: archived image) |
mcpSidecars.homeassistant.enabled |
false |
Home Assistant smart home control via MCP |
mcpSidecars.pgtuner.enabled |
false |
PostgreSQL performance tuning and analysis via MCP |
mcpSidecars.playwright.enabled |
true |
Browser automation and web testing via MCP |
Notes:
- 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 mcpSidecars.kubernetes.enabled=false \
--set mcpSidecars.flux.enabled=false \
--set mcpSidecars.playwright.enabled=false
# Or selectively disable
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set mcpSidecars.flux.enabled=false # Disable only Flux MCP
Enable 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 mcpSidecars.homeassistant.enabled=true
Enable 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 mcpSidecars.pgtuner.enabled=true
Custom MCP configuration:
# values.yaml override
mcpSidecars:
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
github:
enabled: false # Disabled by default (archived image)
homeassistant:
enabled: true
image:
repository: ghcr.io/homeassistant-ai/ha-mcp
tag: stable
port: 8087
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "500m"
pgtuner:
enabled: true
image:
repository: dog830228/pgtuner_mcp
tag: latest
port: 8085
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "500m"
playwright:
enabled: true
image:
repository: microsoft/playwright-mcp
tag: latest
port: 8086
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "1000m"
Display and resources
| Value | Default | Description |
|---|---|---|
display.width |
1920 |
VNC width (px) |
display.height |
1080 |
VNC height (px) |
secureConnection |
0 |
Set to 1 if TLS is not terminated upstream |
userId |
1000 |
UID for the app 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 |
Architecture
Startup flow
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}
→ rm daemon.state.json.lock — clear stale Happy lock
→ happy daemon start — starts Happy Coder background daemon
→ 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)
Storage
| Mount | Source | Persistence |
|---|---|---|
/home |
ReadWriteMany PVC (userhome-{name}) |
Survives pod restarts — stores Claude credentials, dotfiles, git config |
/workspace |
emptyDir |
Ephemeral — repo is re-cloned on each pod start |
Happy Coder's runtime state (HAPPY_HOME_DIR) is kept in /home/user/.happy on the persistent home PVC, so auth credentials and settings survive pod restarts. A stale lock file (daemon.state.json.lock) is removed automatically on each startup.
Troubleshooting
Happy Coder daemon not starting
# Check daemon status
happy daemon status
# Start manually (also clears any stale lock)
happy daemon start
# View daemon logs
ls ~/.happy/logs/
Claude not authenticated
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.
VNC not loading
kubectl port-forward deployment/devcontainer-mydev 5800:5800
kubectl logs deployment/devcontainer-mydev
kubectl describe pod -l app.kubernetes.io/instance=mydev
Pod not picking up new image after upgrade
The 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-mydev
Repository not cloning
kubectl logs deployment/devcontainer-mydev | grep "Repository Initialization"
kubectl exec deployment/devcontainer-mydev -- env | grep GITHUB
Local Docker run
docker 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:latest
Building
docker build -t ghcr.io/cpfarhood/devcontainer:latest .
docker push ghcr.io/cpfarhood/devcontainer:latest
The image is also built and pushed automatically by CI on every push to main and on version tags (v*).
Credits
- Base image: jlesage/docker-baseimage-gui
- AI assistant: Happy Coder + Claude