Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c964e7069 | |||
| d7210fb4e5 | |||
| 7a96f5156c | |||
| 8df46d6b6f | |||
| 5d8b1369c3 | |||
| 751402be44 | |||
| 66e0d1f406 | |||
| e89c3040b7 | |||
| 8d7b39f1b5 | |||
| 32e87254d2 | |||
| 66ccee1202 | |||
| 1909c2a3aa | |||
| d078bb1c44 | |||
| 56c648187a | |||
| 8870d60ccc | |||
| d54515244c | |||
| 2918cfde25 | |||
| dd77cf6a48 | |||
| 961a0985b6 | |||
| d3f5e9f185 | |||
| 9aab08b8e4 | |||
| 727487053d | |||
| 47a275d667 | |||
| 7788352995 | |||
| 81a7098c21 | |||
| 3832fd922b | |||
| 4984e5200c | |||
| c8f51beac6 | |||
| ee7a4a0be8 | |||
| 2472dc5b3f | |||
| 23ba5c2e35 | |||
| 256a6871e8 | |||
| 720a2a982f | |||
| fdfccb17bd | |||
| dfc0a3c1e3 | |||
| 7003d860b7 | |||
| 8eab159e6f | |||
| 13f1878ce5 | |||
| c01f85ef24 | |||
| 3db5961b1c | |||
| 3794ec60d7 | |||
| 17c2d04d70 | |||
| 636b68d263 |
@@ -1,19 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
|
||||
# Maintain dependencies for Docker
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "docker"
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Publish Helm Chart
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'chart/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
|
||||
- name: Bump patch version
|
||||
id: bump
|
||||
run: |
|
||||
CURRENT=$(grep '^version:' chart/Chart.yaml | awk '{print $2}')
|
||||
MAJOR=$(echo $CURRENT | cut -d. -f1)
|
||||
MINOR=$(echo $CURRENT | cut -d. -f2)
|
||||
PATCH=$(echo $CURRENT | cut -d. -f3)
|
||||
NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
|
||||
sed -i "s/^version: .*/version: ${NEW_VERSION}/" chart/Chart.yaml
|
||||
echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit version bump
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add chart/Chart.yaml
|
||||
git commit -m "chore: bump chart version to ${{ steps.bump.outputs.version }} [skip ci]"
|
||||
git push
|
||||
|
||||
- name: Log in to GHCR
|
||||
run: |
|
||||
helm registry login ghcr.io \
|
||||
--username ${{ github.actor }} \
|
||||
--password ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Package chart
|
||||
run: helm package chart/
|
||||
|
||||
- name: Push chart to GHCR
|
||||
run: |
|
||||
helm push devcontainer-${{ steps.bump.outputs.version }}.tgz oci://ghcr.io/cpfarhood/charts
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "github-mcp-server",
|
||||
"args": ["stdio"],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${CLAUDE_GITHUB_TOKEN}"
|
||||
}
|
||||
},
|
||||
"kubernetes (local)": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"kubernetes-mcp-server@latest"
|
||||
]
|
||||
},
|
||||
"flux (local)":{
|
||||
"command":"flux-operator-mcp",
|
||||
"args":["serve"],
|
||||
"env":{
|
||||
"KUBECONFIG":"/Users/cpfarhood/.kube/config"
|
||||
}
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@playwright/mcp@latest"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Antigravity is a Docker-based cloud development environment that provides:
|
||||
- Web-based GUI IDE (VSCode/Antigravity) via VNC on port 5800
|
||||
- Happy Coder AI assistant integration
|
||||
- Automatic GitHub repository cloning on startup
|
||||
- Kubernetes-native deployment with persistent home storage
|
||||
|
||||
The stack is primarily **Bash scripts + YAML** — there is no Node.js package, compiled language, or test framework.
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
make build # Build Docker image
|
||||
make build REGISTRY=ghcr.io/myuser IMAGE_TAG=v1.0 # Custom registry/tag
|
||||
docker build -t ghcr.io/cpfarhood/antigravity:latest . # Direct build
|
||||
```
|
||||
|
||||
### Running Locally
|
||||
|
||||
```bash
|
||||
GITHUB_REPO="https://github.com/user/repo" make run # Run with Docker
|
||||
make stop # Stop container
|
||||
make clean # Remove volumes
|
||||
```
|
||||
|
||||
### Kubernetes Deployment
|
||||
|
||||
```bash
|
||||
make k8s-deploy # Deploy via kustomize
|
||||
kubectl apply -k k8s/ # Direct kustomize apply
|
||||
make k8s-delete # Tear down
|
||||
make k8s-port-forward # Forward port 5800 to localhost
|
||||
make k8s-logs # Stream container logs
|
||||
make k8s-shell # Open interactive shell in pod
|
||||
```
|
||||
|
||||
### Other Useful Targets
|
||||
|
||||
```bash
|
||||
make help # List all Makefile targets with descriptions
|
||||
make push # Push image to registry (build first)
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Startup Flow
|
||||
|
||||
```
|
||||
Container start
|
||||
→ scripts/startapp.sh
|
||||
→ scripts/init-repo.sh (clone GITHUB_REPO, start Happy Coder)
|
||||
→ launch VSCode as user `claude` in /workspace
|
||||
```
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Dockerfile` | Image definition — installs Chrome, Node.js, VSCode, Happy Coder; creates non-root user `claude` (UID 1000) |
|
||||
| `scripts/init-repo.sh` | Clones GitHub repo, authenticates with token, starts Happy Coder background service |
|
||||
| `scripts/startapp.sh` | Calls init-repo.sh then opens VSCode in the workspace |
|
||||
| `k8s/statefulset.yaml` | StatefulSet + headless Service; mounts `/home` (PVC) and `/workspace` (emptyDir) |
|
||||
| `k8s/configmap.yaml` | `GITHUB_REPO`, `HAPPY_SERVER_URL`, `HAPPY_WEBAPP_URL` |
|
||||
| `k8s/httproute.yaml` | Gateway API HTTPRoute for external browser access |
|
||||
| `k8s/secrets-example.yaml` | Template for SealedSecrets (GitHub token, VNC password) |
|
||||
| `Makefile` | Build/deploy automation |
|
||||
|
||||
### Storage Model
|
||||
|
||||
- `/home` — ReadWriteMany PVC (persists across pod restarts, holds user config/dotfiles)
|
||||
- `/workspace` — emptyDir by default (ephemeral; can be changed to PVC)
|
||||
|
||||
### Environment Variables
|
||||
|
||||
**Required:**
|
||||
- `GITHUB_REPO` — URL of repository to clone into `/workspace`
|
||||
|
||||
**Optional:**
|
||||
- `GITHUB_TOKEN` — PAT for private repo access
|
||||
- `VNC_PASSWORD` — VNC web interface password
|
||||
- `DISPLAY_WIDTH` / `DISPLAY_HEIGHT` — VNC resolution
|
||||
- `USER_ID` / `GROUP_ID` — Override UID/GID (default 1000)
|
||||
- `HAPPY_SERVER_URL` / `HAPPY_WEBAPP_URL` — Custom Happy Coder endpoints
|
||||
- `HAPPY_HOME_DIR` / `HAPPY_EXPERIMENTAL`
|
||||
|
||||
### CI/CD
|
||||
|
||||
- **`build-and-push.yaml`** — Builds and pushes to GHCR on every push to `main`, version tags (`v*`), and PRs. Tags: `latest` (main), semver, branch name, commit SHA.
|
||||
- **`release.yaml`** — Creates a GitHub Release with docker pull instructions when a version tag is pushed.
|
||||
- **`dependabot.yml`** — Weekly updates for GitHub Actions and Docker base image.
|
||||
|
||||
Image registry: `ghcr.io/cpfarhood/devcontainer`
|
||||
|
||||
## Kubernetes Notes
|
||||
|
||||
- Uses Kustomize (`kubectl apply -k k8s/`)
|
||||
- Storage class is `ceph-filesystem` by default — change in `statefulset.yaml` for other clusters
|
||||
- Resource limits: 1–4 CPU, 2–8Gi memory
|
||||
- Health checks (liveness/readiness probes) on port 5800
|
||||
- Secrets managed via SealedSecrets (see `k8s/secrets-example.yaml`)
|
||||
+23
-14
@@ -8,7 +8,7 @@ ENV APP_NAME="Antigravity Dev Container" \
|
||||
SECURE_CONNECTION=1 \
|
||||
USER_ID=1000 \
|
||||
GROUP_ID=1000 \
|
||||
CLAUDE_USER=claude
|
||||
CLAUDE_USER=user
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
@@ -25,20 +25,26 @@ RUN apt-get update && apt-get install -y \
|
||||
sudo \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Chrome
|
||||
# Install Chrome and xdg-utils (needed for xdg-open to work in VNC)
|
||||
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome-keyring.gpg && \
|
||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y google-chrome-stable && \
|
||||
apt-get install -y google-chrome-stable xdg-utils && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Chrome wrapper: adds flags required for running inside a Docker container.
|
||||
# xdg-open (used by Claude Code on Linux) respects $BROWSER, so pointing it
|
||||
# here ensures the OAuth popup works without manual --no-sandbox invocations.
|
||||
RUN printf '#!/bin/bash\nexec /usr/bin/google-chrome-stable \\\n --no-sandbox \\\n --disable-dev-shm-usage \\\n --disable-gpu \\\n "$@"\n' > /usr/local/bin/google-chrome && \
|
||||
chmod +x /usr/local/bin/google-chrome
|
||||
|
||||
# Install Node.js (LTS version for Happy Coder)
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
|
||||
apt-get install -y nodejs && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Happy Coder globally
|
||||
RUN npm install -g happy-coder
|
||||
# Install Happy Coder and Claude Code globally
|
||||
RUN npm install -g happy-coder @anthropic-ai/claude-code
|
||||
|
||||
# Install Antigravity (Google's Project IDX / Cloud Code alternative)
|
||||
# Note: Antigravity might be packaged differently - adjust as needed
|
||||
@@ -49,25 +55,28 @@ RUN wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor
|
||||
apt-get install -y code && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create claude user with specific UID/GID
|
||||
RUN groupadd -g 1000 claude && \
|
||||
useradd -u 1000 -g 1000 -m -s /bin/bash claude && \
|
||||
echo "claude ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
||||
# Create user user with specific UID/GID
|
||||
RUN groupadd -g 1000 user && \
|
||||
useradd -u 1000 -g 1000 -m -s /bin/bash user && \
|
||||
echo "user ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
||||
|
||||
# Create workspace directory
|
||||
RUN mkdir -p /workspace && \
|
||||
chown -R claude:claude /workspace
|
||||
chown -R user:user /workspace
|
||||
|
||||
# Copy startup script
|
||||
# Copy startup scripts
|
||||
COPY --chmod=755 scripts/startapp.sh /startapp.sh
|
||||
COPY --chmod=755 scripts/init-repo.sh /usr/local/bin/init-repo
|
||||
# Fix app user shell after baseimage-gui creates it at runtime
|
||||
COPY --chmod=755 scripts/cont-init-user.sh /etc/cont-init.d/20-fix-user-shell.sh
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /workspace
|
||||
|
||||
# Configure container to run as claude user
|
||||
ENV HOME=/home/claude \
|
||||
USER=claude
|
||||
# Configure container to run as user user
|
||||
ENV HOME=/home/user \
|
||||
USER=user \
|
||||
BROWSER=/usr/local/bin/google-chrome
|
||||
|
||||
# Expose VNC port (baseimage-gui default)
|
||||
EXPOSE 5800
|
||||
|
||||
@@ -2,366 +2,227 @@
|
||||
|
||||

|
||||
|
||||
A containerized development environment with GUI access, featuring:
|
||||
- **Antigravity** (VSCode/Cloud IDE) via web browser
|
||||
- **Happy Coder** - AI-powered development assistant
|
||||
- **Automatic GitHub repo cloning**
|
||||
- **Persistent user home directory**
|
||||
- **Secure non-root execution**
|
||||
|
||||
## Features
|
||||
|
||||
### GUI Access
|
||||
- Web-based VNC interface (port 5800)
|
||||
- Full desktop environment in your browser
|
||||
- Secure connections with optional password protection
|
||||
|
||||
### Development Tools
|
||||
- Antigravity IDE (VSCode-based)
|
||||
- Happy Coder AI assistant
|
||||
- Git integration
|
||||
- Node.js and npm
|
||||
- Python 3
|
||||
- Chrome browser
|
||||
|
||||
### Security
|
||||
- Runs as non-root user `claude` (UID 1000, GID 1000)
|
||||
- Secure VNC connections
|
||||
- Token-based GitHub authentication
|
||||
- Isolated workspace
|
||||
|
||||
### Persistence
|
||||
- ReadWriteMany PVC for `/home` (user data persists)
|
||||
- Workspace mounted at `/workspace`
|
||||
- Repository cloned on first startup
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[DEPLOYMENT.md](DEPLOYMENT.md)** - Complete deployment guide with step-by-step instructions
|
||||
- **[VARIABLES.md](VARIABLES.md)** - Reference for all configuration variables
|
||||
- **[README.md](README.md)** - This file (overview and quick start)
|
||||
A containerized cloud development environment with web-based GUI access, featuring:
|
||||
- **VSCode** via browser-based VNC (port 5800)
|
||||
- **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
|
||||
|
||||
**👉 For detailed deployment instructions, see [DEPLOYMENT.md](DEPLOYMENT.md)**
|
||||
### 1. Create a secret
|
||||
|
||||
### 1. Get the Image
|
||||
The secret is picked up automatically via `envFrom`. Keys recognised:
|
||||
|
||||
The image is automatically built and published to GitHub Container Registry on every push to main.
|
||||
| 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 |
|
||||
|
||||
```bash
|
||||
# Pull the latest image
|
||||
docker pull ghcr.io/cpfarhood/devcontainer:latest
|
||||
|
||||
# Or pull a specific version
|
||||
docker pull ghcr.io/cpfarhood/devcontainer:v1.0.0
|
||||
kubectl create secret generic devcontainer-mydev-secrets-env \
|
||||
--from-literal=GITHUB_TOKEN='ghp_...' \
|
||||
--from-literal=VNC_PASSWORD='changeme'
|
||||
```
|
||||
|
||||
**Building locally (optional):**
|
||||
Or use SealedSecrets:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
helm install mydev ./chart \
|
||||
--set name=mydev \
|
||||
--set githubRepo=https://github.com/youruser/yourrepo
|
||||
```
|
||||
|
||||
### 3. Access
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
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 |
|
||||
| `image.repository` | `ghcr.io/cpfarhood/devcontainer` | Container image |
|
||||
| `image.tag` | `latest` | Image tag |
|
||||
|
||||
### 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` | `/workspace/.happy` | Happy runtime state directory (ephemeral — lives in emptyDir) |
|
||||
| `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 | `*` |
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
|
||||
### 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) |
|
||||
| `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 (as app user, UID 1000)
|
||||
→ cont-init.d/20-fix-user-shell.sh — fix shell/home on baseimage-gui app user
|
||||
→ /startapp.sh
|
||||
→ init-repo.sh
|
||||
→ clone / pull GITHUB_REPO into /workspace/{repo}
|
||||
→ happy daemon start — starts Happy Coder background daemon
|
||||
→ code --new-window /workspace/{repo} — opens VSCode in VNC
|
||||
```
|
||||
|
||||
### 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 `/workspace/.happy` so stale lock files never survive a pod restart.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Happy Coder daemon not starting
|
||||
|
||||
```bash
|
||||
# Check daemon status
|
||||
happy daemon status
|
||||
|
||||
# Start manually (also clears any stale lock)
|
||||
happy daemon start
|
||||
|
||||
# View daemon logs
|
||||
ls ~/.happy/logs/ || ls /workspace/.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:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
kubectl port-forward deployment/devcontainer-mydev 5800:5800
|
||||
kubectl logs deployment/devcontainer-mydev
|
||||
kubectl describe pod -l instance=mydev
|
||||
```
|
||||
|
||||
### Repository not cloning
|
||||
|
||||
```bash
|
||||
kubectl logs deployment/devcontainer-mydev | grep "Repository Initialization"
|
||||
kubectl exec deployment/devcontainer-mydev -- env | grep GITHUB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Local Docker run
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
docker build -t ghcr.io/cpfarhood/devcontainer:latest .
|
||||
docker push ghcr.io/cpfarhood/devcontainer:latest
|
||||
```
|
||||
|
||||
### 2. Configure Secrets
|
||||
The image is also built and pushed automatically by CI on every push to `main` and on version tags (`v*`).
|
||||
|
||||
Edit `k8s/secrets-example.yaml` and create a sealed secret:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic antigravity-secrets \
|
||||
--from-literal=github-token='ghp_your_token' \
|
||||
--from-literal=vnc-password='your_password' \
|
||||
--dry-run=client -o yaml | \
|
||||
kubeseal --format=yaml > k8s/sealedsecrets.yaml
|
||||
```
|
||||
|
||||
### 3. Configure Repository
|
||||
|
||||
Edit `k8s/configmap.yaml`:
|
||||
|
||||
```yaml
|
||||
data:
|
||||
github-repo: "https://github.com/yourusername/yourrepo"
|
||||
```
|
||||
|
||||
### 4. Deploy to Kubernetes
|
||||
|
||||
```bash
|
||||
kubectl apply -k k8s/
|
||||
```
|
||||
|
||||
### 5. Access the Interface
|
||||
|
||||
```bash
|
||||
# Port forward for local access
|
||||
kubectl port-forward statefulset/antigravity 5800:5800
|
||||
|
||||
# Open in browser
|
||||
open http://localhost:5800
|
||||
```
|
||||
|
||||
Or configure HTTPRoute (Gateway API) for external access via your domain.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Required
|
||||
- `GITHUB_REPO` - GitHub repository URL to clone
|
||||
|
||||
### Optional
|
||||
- `GITHUB_TOKEN` - GitHub Personal Access Token (for private repos)
|
||||
- `VNC_PASSWORD` - Password for VNC access
|
||||
- `USER_ID` - UID for claude user (default: 1000)
|
||||
- `GROUP_ID` - GID for claude user (default: 1000)
|
||||
- `DISPLAY_WIDTH` - VNC display width (default: 1920)
|
||||
- `DISPLAY_HEIGHT` - VNC display height (default: 1080)
|
||||
|
||||
### Happy Coder Configuration (Optional)
|
||||
- `HAPPY_SERVER_URL` - Custom Happy server URL (default: https://api.cluster-fluster.com)
|
||||
- `HAPPY_WEBAPP_URL` - Custom Happy webapp URL (default: https://app.happy.engineering)
|
||||
- `HAPPY_HOME_DIR` - Happy data directory (default: /home/claude/.happy)
|
||||
- `HAPPY_EXPERIMENTAL` - Enable experimental features (default: true in container)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Web Browser (Port 5800) │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ VNC Web Interface │
|
||||
│ (jlesage/baseimage-gui) │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ Antigravity IDE │
|
||||
│ (VSCode + Extensions) │
|
||||
│ Running as user: claude (1000) │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ Happy Coder (Background Process) │
|
||||
│ AI Development Assistant │
|
||||
└─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ Workspace: /workspace/{repo} │
|
||||
│ Home: /home/claude (RWX PVC) │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Startup Flow
|
||||
|
||||
1. **Container starts** - baseimage-gui initializes
|
||||
2. **init-repo.sh runs**:
|
||||
- Checks for `GITHUB_REPO` environment variable
|
||||
- Clones repository to `/workspace/{repo-name}` if not exists
|
||||
- Configures git credentials with `GITHUB_TOKEN`
|
||||
- Starts Happy Coder in background
|
||||
3. **startapp.sh runs**:
|
||||
- Opens Antigravity IDE in the cloned repository
|
||||
- Happy Coder is already running and accessible
|
||||
|
||||
## Happy Coder Integration
|
||||
|
||||
Happy Coder runs as a background service and is accessible within the IDE:
|
||||
|
||||
```bash
|
||||
# Check Happy Coder status
|
||||
ps aux | grep happy-coder
|
||||
|
||||
# View logs
|
||||
cat /tmp/happy-coder.log
|
||||
|
||||
# Restart Happy Coder
|
||||
sudo -u claude bash -c "cd /workspace/your-repo && happy-coder &"
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
### Run with Docker Compose
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
antigravity:
|
||||
build: .
|
||||
ports:
|
||||
- "5800:5800"
|
||||
environment:
|
||||
- GITHUB_REPO=https://github.com/yourusername/yourrepo
|
||||
- GITHUB_TOKEN=ghp_your_token
|
||||
- VNC_PASSWORD=yourpassword
|
||||
- HAPPY_EXPERIMENTAL=true
|
||||
volumes:
|
||||
- ./home:/home
|
||||
- ./workspace:/workspace
|
||||
```
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
### Run with Docker
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
-p 5800:5800 \
|
||||
-e GITHUB_REPO="https://github.com/yourusername/yourrepo" \
|
||||
-e GITHUB_TOKEN="ghp_your_token" \
|
||||
-e VNC_PASSWORD="yourpassword" \
|
||||
-e HAPPY_EXPERIMENTAL="true" \
|
||||
-v $(pwd)/home:/home \
|
||||
-v $(pwd)/workspace:/workspace \
|
||||
ghcr.io/cpfarhood/antigravity:latest
|
||||
```
|
||||
|
||||
## Kubernetes Deployment
|
||||
|
||||
### With Flux
|
||||
|
||||
See the animaniacs cluster configuration for GitOps deployment patterns.
|
||||
|
||||
### Standalone
|
||||
|
||||
```bash
|
||||
# Apply manifests
|
||||
kubectl apply -k k8s/
|
||||
|
||||
# Check status
|
||||
kubectl get statefulset antigravity
|
||||
kubectl get pods -l app=antigravity
|
||||
|
||||
# Access logs
|
||||
kubectl logs antigravity-0
|
||||
|
||||
# Access shell
|
||||
kubectl exec -it antigravity-0 -- bash
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Repository not cloning
|
||||
|
||||
```bash
|
||||
# Check logs
|
||||
kubectl logs antigravity-0 | grep "Repository Initialization"
|
||||
|
||||
# Verify GITHUB_REPO is set
|
||||
kubectl exec antigravity-0 -- env | grep GITHUB
|
||||
|
||||
# Check git credentials
|
||||
kubectl exec antigravity-0 -- cat /home/claude/.git-credentials
|
||||
```
|
||||
|
||||
### Happy Coder not starting
|
||||
|
||||
```bash
|
||||
# Check Happy Coder logs
|
||||
kubectl exec antigravity-0 -- cat /tmp/happy-coder.log
|
||||
|
||||
# Verify API key
|
||||
kubectl exec antigravity-0 -- env | grep HAPPY_CODER
|
||||
|
||||
# Restart Happy Coder
|
||||
kubectl exec antigravity-0 -- sudo -u claude bash -c "cd /workspace/repo && happy-coder &"
|
||||
```
|
||||
|
||||
### VNC not accessible
|
||||
|
||||
```bash
|
||||
# Check port forwarding
|
||||
kubectl port-forward antigravity-0 5800:5800
|
||||
|
||||
# Verify service
|
||||
kubectl get svc antigravity
|
||||
|
||||
# Check pod status
|
||||
kubectl describe pod antigravity-0
|
||||
```
|
||||
|
||||
### Permission issues
|
||||
|
||||
```bash
|
||||
# Check ownership
|
||||
kubectl exec antigravity-0 -- ls -la /home/claude
|
||||
kubectl exec antigravity-0 -- ls -la /workspace
|
||||
|
||||
# Fix ownership
|
||||
kubectl exec antigravity-0 -- chown -R claude:claude /home/claude
|
||||
kubectl exec antigravity-0 -- chown -R claude:claude /workspace
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Secrets Management**: Use SealedSecrets or external secret managers
|
||||
2. **Network Policies**: Restrict ingress/egress as needed
|
||||
3. **RBAC**: Limit who can access the namespace
|
||||
4. **VNC Password**: Always set a strong VNC password
|
||||
5. **GitHub Token**: Use fine-grained tokens with minimal permissions
|
||||
6. **Container Security**: Runs as non-root user (claude:1000)
|
||||
|
||||
## Storage
|
||||
|
||||
### Home Directory (`/home`)
|
||||
- Mounted from ReadWriteMany PVC (`userhome`)
|
||||
- Persists user settings, credentials, history
|
||||
- Survives pod restarts
|
||||
|
||||
### Workspace (`/workspace`)
|
||||
- ephemeral emptyDir (can be changed to PVC)
|
||||
- Contains cloned repository
|
||||
- Rebuild on pod restart
|
||||
|
||||
To persist workspace:
|
||||
1. Create a PVC for workspace
|
||||
2. Update `statefulset.yaml` to use PVC instead of emptyDir
|
||||
|
||||
## Customization
|
||||
|
||||
### Add More Tools
|
||||
|
||||
Edit `Dockerfile`:
|
||||
|
||||
```dockerfile
|
||||
RUN apt-get update && apt-get install -y \
|
||||
your-package-here \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
```
|
||||
|
||||
### Change Display Resolution
|
||||
|
||||
Set environment variables:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
- name: DISPLAY_WIDTH
|
||||
value: "2560"
|
||||
- name: DISPLAY_HEIGHT
|
||||
value: "1440"
|
||||
```
|
||||
|
||||
### Auto-clone Multiple Repos
|
||||
|
||||
Modify `init-repo.sh` to support `GITHUB_REPOS` (comma-separated):
|
||||
|
||||
```bash
|
||||
IFS=',' read -ra REPOS <<< "$GITHUB_REPOS"
|
||||
for repo in "${REPOS[@]}"; do
|
||||
# Clone each repo
|
||||
done
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
- Built on [jlesage/baseimage-gui](https://github.com/jlesage/docker-baseimage-gui)
|
||||
- Uses [Happy Coder](https://happy.engineering)
|
||||
- Inspired by Google's Project IDX
|
||||
- Base image: [jlesage/docker-baseimage-gui](https://github.com/jlesage/docker-baseimage-gui)
|
||||
- AI assistant: [Happy Coder](https://happy.engineering) + [Claude](https://claude.ai)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: devcontainer
|
||||
description: Antigravity Dev Container with Happy Coder AI assistant
|
||||
type: application
|
||||
version: 0.1.2
|
||||
appVersion: "latest"
|
||||
@@ -0,0 +1,28 @@
|
||||
{{/*
|
||||
Resource name prefix: devcontainer-{name}
|
||||
*/}}
|
||||
{{- define "antigravity.fullname" -}}
|
||||
{{- printf "devcontainer-%s" .Values.name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
PVC name: userhome-{name}
|
||||
*/}}
|
||||
{{- define "antigravity.pvcName" -}}
|
||||
{{- printf "userhome-%s" .Values.name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Secret name for env vars, default to devcontainer-{name}-secrets-env
|
||||
*/}}
|
||||
{{- define "antigravity.envSecretName" -}}
|
||||
{{- .Values.envSecretName | default (printf "devcontainer-%s-secrets-env" .Values.name) }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "antigravity.labels" -}}
|
||||
app: devcontainer
|
||||
instance: {{ .Values.name }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,80 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "antigravity.fullname" . }}
|
||||
labels:
|
||||
{{- include "antigravity.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "antigravity.labels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "antigravity.labels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- if ne (.Values.clusterAccess | default "none") "none" }}
|
||||
serviceAccountName: {{ include "antigravity.fullname" . }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
fsGroup: 1000
|
||||
fsGroupChangePolicy: "OnRootMismatch"
|
||||
containers:
|
||||
- name: devcontainer
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: 5800
|
||||
name: vnc-web
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: USER_ID
|
||||
value: {{ .Values.userId | quote }}
|
||||
- name: GROUP_ID
|
||||
value: {{ .Values.groupId | quote }}
|
||||
- name: DISPLAY_WIDTH
|
||||
value: {{ .Values.display.width | quote }}
|
||||
- name: DISPLAY_HEIGHT
|
||||
value: {{ .Values.display.height | quote }}
|
||||
- name: SECURE_CONNECTION
|
||||
value: {{ .Values.secureConnection | quote }}
|
||||
- name: HAPPY_HOME_DIR
|
||||
value: {{ .Values.happyHomeDir | quote }}
|
||||
- name: HAPPY_EXPERIMENTAL
|
||||
value: {{ .Values.happyExperimental | quote }}
|
||||
- name: HAPPY_SERVER_URL
|
||||
value: {{ .Values.happyServerUrl | quote }}
|
||||
- name: HAPPY_WEBAPP_URL
|
||||
value: {{ .Values.happyWebappUrl | quote }}
|
||||
- name: GITHUB_REPO
|
||||
value: {{ .Values.githubRepo | quote }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: {{ include "antigravity.envSecretName" . }}
|
||||
optional: true
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: userhome
|
||||
mountPath: /home
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 5800
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 5800
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: workspace
|
||||
emptyDir: {}
|
||||
- name: userhome
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "antigravity.pvcName" . }}
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "antigravity.pvcName" . }}
|
||||
labels:
|
||||
{{- include "antigravity.labels" . | nindent 4 }}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
storageClassName: {{ .Values.storage.className }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.storage.size }}
|
||||
@@ -0,0 +1,97 @@
|
||||
{{- $access := .Values.clusterAccess | default "none" }}
|
||||
{{- $name := include "antigravity.fullname" . }}
|
||||
{{- $ns := .Release.Namespace }}
|
||||
{{- $labels := include "antigravity.labels" . }}
|
||||
|
||||
{{- if ne $access "none" }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $ns }}
|
||||
labels:
|
||||
{{- $labels | nindent 4 }}
|
||||
|
||||
{{- if or (eq $access "readonlyns") (eq $access "readwritens") }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $ns }}
|
||||
labels:
|
||||
{{- $labels | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups: ["*"]
|
||||
resources: ["*"]
|
||||
verbs:
|
||||
{{- if eq $access "readonlyns" }}
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
{{- else }}
|
||||
- "*"
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
namespace: {{ $ns }}
|
||||
labels:
|
||||
{{- $labels | nindent 4 }}
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ $name }}
|
||||
namespace: {{ $ns }}
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: {{ $name }}
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{- end }}
|
||||
|
||||
{{- if or (eq $access "readonly") (eq $access "readwrite") }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
labels:
|
||||
{{- $labels | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups: ["*"]
|
||||
resources: ["*"]
|
||||
verbs:
|
||||
{{- if eq $access "readonly" }}
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
{{- else }}
|
||||
- "*"
|
||||
{{- end }}
|
||||
- nonResourceURLs: ["*"]
|
||||
verbs:
|
||||
{{- if eq $access "readonly" }}
|
||||
- get
|
||||
{{- else }}
|
||||
- "*"
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: {{ $name }}
|
||||
labels:
|
||||
{{- $labels | nindent 4 }}
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ $name }}
|
||||
namespace: {{ $ns }}
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: {{ $name }}
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{- end }}
|
||||
|
||||
{{- end }}
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "antigravity.fullname" . }}
|
||||
labels:
|
||||
{{- include "antigravity.labels" . | nindent 4 }}
|
||||
spec:
|
||||
ports:
|
||||
- port: 5800
|
||||
name: vnc-web
|
||||
protocol: TCP
|
||||
targetPort: vnc-web
|
||||
selector:
|
||||
{{- include "antigravity.labels" . | nindent 4 }}
|
||||
@@ -0,0 +1,52 @@
|
||||
# Instance name — used to generate resource names (devcontainer-{name}, userhome-{name})
|
||||
name: ""
|
||||
|
||||
image:
|
||||
repository: ghcr.io/cpfarhood/devcontainer
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
|
||||
# GitHub repository to clone into /workspace
|
||||
githubRepo: ""
|
||||
|
||||
# Happy Coder endpoints
|
||||
happyServerUrl: "https://happy.farh.net"
|
||||
happyWebappUrl: "https://happy-coder.farh.net"
|
||||
happyHomeDir: "/workspace/.happy"
|
||||
happyExperimental: "true"
|
||||
|
||||
# VNC display
|
||||
display:
|
||||
width: "1920"
|
||||
height: "1080"
|
||||
|
||||
# Set to "0" when TLS is terminated at the gateway layer
|
||||
secureConnection: "0"
|
||||
|
||||
userId: "1000"
|
||||
groupId: "1000"
|
||||
|
||||
storage:
|
||||
size: 32Gi
|
||||
className: ceph-filesystem
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: "2Gi"
|
||||
cpu: "1000m"
|
||||
limits:
|
||||
memory: "8Gi"
|
||||
cpu: "4000m"
|
||||
|
||||
# Kubernetes cluster access granted to the devcontainer pod via RBAC.
|
||||
# Options:
|
||||
# none — no cluster access (default)
|
||||
# readonlyns — get/list/watch all resources in the release namespace
|
||||
# readwritens — full access to all resources in the release namespace
|
||||
# readonly — get/list/watch all resources cluster-wide
|
||||
# readwrite — full access to all resources cluster-wide
|
||||
clusterAccess: none
|
||||
|
||||
# Name of existing Secret containing env vars (GITHUB_TOKEN, VNC_PASSWORD, etc.)
|
||||
# Defaults to: devcontainer-{name}-secrets-env
|
||||
envSecretName: ""
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: antigravity-config
|
||||
data:
|
||||
# GitHub repository to clone on startup
|
||||
# Example: "https://github.com/username/repository"
|
||||
github-repo: ""
|
||||
|
||||
# Happy Coder configuration (optional)
|
||||
# happy-server-url: "https://api.cluster-fluster.com"
|
||||
# happy-webapp-url: "https://app.happy.engineering"
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: antigravity
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: gateway # Replace with your Gateway name
|
||||
namespace: gateway-system # Replace with your Gateway namespace
|
||||
hostnames:
|
||||
- "antigravity.example.com" # Replace with your domain
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
backendRefs:
|
||||
- name: antigravity
|
||||
port: 5800
|
||||
weight: 1
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: default
|
||||
|
||||
resources:
|
||||
- configmap.yaml
|
||||
- statefulset.yaml
|
||||
- httproute.yaml
|
||||
|
||||
# Uncomment to create secrets from files
|
||||
# secretGenerator:
|
||||
# - name: antigravity-secrets
|
||||
# literals:
|
||||
# - github-token=ghp_your_token
|
||||
# - vnc-password=your_password
|
||||
|
||||
commonLabels:
|
||||
app: antigravity
|
||||
environment: production
|
||||
|
||||
commonAnnotations:
|
||||
managed-by: kustomize
|
||||
description: "Antigravity Dev Container with Happy Coder"
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
# Example secrets - DO NOT commit actual secrets!
|
||||
# Use SealedSecrets or another secret management solution
|
||||
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: antigravity-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
# GitHub Personal Access Token (for private repos)
|
||||
github-token: "ghp_your_token_here"
|
||||
|
||||
# VNC Password (optional, for secure VNC access)
|
||||
vnc-password: "your_vnc_password"
|
||||
@@ -1,127 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: antigravity
|
||||
labels:
|
||||
app: antigravity
|
||||
spec:
|
||||
ports:
|
||||
- port: 5800
|
||||
name: vnc-web
|
||||
protocol: TCP
|
||||
clusterIP: None
|
||||
selector:
|
||||
app: antigravity
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: antigravity
|
||||
spec:
|
||||
serviceName: "antigravity"
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: antigravity
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: antigravity
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 1000
|
||||
fsGroupChangePolicy: "OnRootMismatch"
|
||||
containers:
|
||||
- name: antigravity
|
||||
image: ghcr.io/cpfarhood/antigravity:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 5800
|
||||
name: vnc-web
|
||||
protocol: TCP
|
||||
volumeMounts:
|
||||
- name: userhome
|
||||
mountPath: /home
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
env:
|
||||
# User/Group IDs for the claude user
|
||||
- name: USER_ID
|
||||
value: "1000"
|
||||
- name: GROUP_ID
|
||||
value: "1000"
|
||||
# VNC display settings
|
||||
- name: DISPLAY_WIDTH
|
||||
value: "1920"
|
||||
- name: DISPLAY_HEIGHT
|
||||
value: "1080"
|
||||
- name: SECURE_CONNECTION
|
||||
value: "1"
|
||||
- name: VNC_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: antigravity
|
||||
key: vnc-password
|
||||
optional: true
|
||||
# GitHub configuration
|
||||
- name: GITHUB_REPO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: antigravity
|
||||
key: github-repo
|
||||
optional: true
|
||||
- name: GITHUB_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: antigravity
|
||||
key: github-token
|
||||
optional: true
|
||||
# Happy Coder configuration (optional)
|
||||
- name: HAPPY_SERVER_URL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: antigravity
|
||||
key: happy-server-url
|
||||
optional: true
|
||||
- name: HAPPY_WEBAPP_URL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: antigravity
|
||||
key: happy-webapp-url
|
||||
optional: true
|
||||
- name: HAPPY_HOME_DIR
|
||||
value: "/home/claude/.happy"
|
||||
- name: HAPPY_EXPERIMENTAL
|
||||
value: "true"
|
||||
resources:
|
||||
requests:
|
||||
memory: "2Gi"
|
||||
cpu: "1000m"
|
||||
limits:
|
||||
memory: "8Gi"
|
||||
cpu: "4000m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 5800
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 5800
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: workspace
|
||||
emptyDir: {}
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: userhome
|
||||
spec:
|
||||
accessModes: [ "ReadWriteMany" ]
|
||||
storageClassName: "ceph-filesystem"
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
@@ -0,0 +1,20 @@
|
||||
# Antigravity Dev Container - Session Notes
|
||||
|
||||
## Key Architecture Facts
|
||||
- Image: `ghcr.io/cpfarhood/devcontainer:latest` (repo name is `devcontainer`, not `antigravity`)
|
||||
- `imagePullPolicy: Always` in statefulset (set during initial deployment debugging)
|
||||
- Service must NOT be headless (`clusterIP: None`) — Cilium gateway can't route to headless services
|
||||
- `SECURE_CONNECTION=0` — TLS is terminated at the gateway, not the app
|
||||
- Container user is `user` (UID 1000) — baseimage-gui runs startapp.sh as `app` user, sudo is not available
|
||||
- HTTPRoute is managed by Authentik outpost, not in kustomization
|
||||
|
||||
## Cluster Patterns
|
||||
- External gateway: `external` in `gateway-system`, handles `*.farh.net` on port 443 HTTPS only
|
||||
- Hostnames must be exactly `*.farh.net` (not `*.subdomain.farh.net`) to match gateway listener
|
||||
- Authentik outpost Terraform lives in `../kubernetes/terraform/authentik-*-proxy/`
|
||||
- Outpost config uses `external` gateway for public apps, `internal` for internal apps
|
||||
|
||||
## Common Gotchas
|
||||
- `baseimage-gui` creates user dynamically — don't hardcode usernames in scripts, use numeric UID/GID
|
||||
- `chown /home` fails (PVC root not owned by container) — only chown subdirectories
|
||||
- `sudo` not available in startapp.sh — script already runs as correct user
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":gitSignOff"
|
||||
],
|
||||
"semanticCommits": "enabled",
|
||||
"dependencyDashboard": true,
|
||||
"suppressNotifications": [
|
||||
"prEditedNotification"
|
||||
],
|
||||
"rebaseWhen": "conflicted",
|
||||
"commitMessagePrefix": "chore(deps):",
|
||||
"commitMessageAction": "update",
|
||||
"commitMessageTopic": "{{depName}}",
|
||||
"prConcurrentLimit": 5,
|
||||
"prHourlyLimit": 2,
|
||||
"schedule": [
|
||||
"before 6am on monday"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "GitHub Actions",
|
||||
"matchManagers": [
|
||||
"github-actions"
|
||||
],
|
||||
"groupName": "github-actions",
|
||||
"additionalBranchPrefix": "github-actions-",
|
||||
"semanticCommitScope": "github-actions",
|
||||
"pinDigests": true
|
||||
},
|
||||
{
|
||||
"description": "Docker base image",
|
||||
"matchManagers": [
|
||||
"dockerfile"
|
||||
],
|
||||
"groupName": "docker",
|
||||
"additionalBranchPrefix": "docker-",
|
||||
"semanticCommitScope": "docker"
|
||||
},
|
||||
{
|
||||
"description": "Automerge patch updates",
|
||||
"matchUpdateTypes": [
|
||||
"patch"
|
||||
],
|
||||
"automerge": true,
|
||||
"automergeType": "pr",
|
||||
"platformAutomerge": true
|
||||
},
|
||||
{
|
||||
"description": "Automerge minor updates for stable packages",
|
||||
"matchUpdateTypes": [
|
||||
"minor"
|
||||
],
|
||||
"matchCurrentVersion": "!/^0/",
|
||||
"automerge": true,
|
||||
"automergeType": "pr",
|
||||
"platformAutomerge": true
|
||||
},
|
||||
{
|
||||
"description": "Separate major updates - require manual review",
|
||||
"matchUpdateTypes": [
|
||||
"major"
|
||||
],
|
||||
"automerge": false,
|
||||
"additionalBranchPrefix": "major-"
|
||||
}
|
||||
],
|
||||
"ignorePaths": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
# Fix the app user (UID 1000) created by baseimage-gui at runtime.
|
||||
# baseimage-gui sets shell=/sbin/nologin and home=/dev/null, which
|
||||
# prevents VSCode from opening terminals.
|
||||
usermod -s /bin/bash app
|
||||
usermod -d /home/user app
|
||||
+22
-21
@@ -25,8 +25,8 @@ else
|
||||
# Configure git to use token if provided
|
||||
if [ -n "$GITHUB_TOKEN" ]; then
|
||||
git config credential.helper store
|
||||
echo "https://oauth2:${GITHUB_TOKEN}@github.com" > /home/claude/.git-credentials
|
||||
chmod 600 /home/claude/.git-credentials
|
||||
echo "https://oauth2:${GITHUB_TOKEN}@github.com" > /home/.git-credentials
|
||||
chmod 600 /home/.git-credentials
|
||||
fi
|
||||
|
||||
git pull || echo "Pull failed, continuing anyway..."
|
||||
@@ -42,35 +42,36 @@ else
|
||||
|
||||
# Configure credentials for future use
|
||||
git config --global credential.helper store
|
||||
echo "https://oauth2:${GITHUB_TOKEN}@github.com" > /home/claude/.git-credentials
|
||||
chmod 600 /home/claude/.git-credentials
|
||||
echo "https://oauth2:${GITHUB_TOKEN}@github.com" > /home/.git-credentials
|
||||
chmod 600 /home/.git-credentials
|
||||
else
|
||||
git clone "$GITHUB_REPO" "$WORKSPACE_DIR"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set ownership
|
||||
chown -R claude:claude "$WORKSPACE_DIR"
|
||||
chown -R claude:claude /home/claude
|
||||
# Set ownership using numeric IDs (username may not exist yet in baseimage-gui)
|
||||
RUN_UID="${USER_ID:-1000}"
|
||||
RUN_GID="${GROUP_ID:-1000}"
|
||||
chown -R "$RUN_UID:$RUN_GID" "$WORKSPACE_DIR"
|
||||
|
||||
# Start Happy Coder in background as claude user
|
||||
# Ensure home directory exists on the PVC (may be absent on a fresh volume)
|
||||
mkdir -p "$HOME"
|
||||
chown "$RUN_UID:$RUN_GID" "$HOME"
|
||||
|
||||
# Start Happy Coder daemon. startapp.sh already runs as the app user (UID 1000),
|
||||
# so no sudo needed — Happy/Claude Code will find credentials in the correct home dir.
|
||||
echo "Starting Happy Coder..."
|
||||
|
||||
# HAPPY_HOME_DIR is in /workspace (emptyDir), so it is always fresh on pod start.
|
||||
# Remove the lock file as a safety net in case daemon start is called more than
|
||||
# once within the same container lifetime.
|
||||
rm -f "${HAPPY_HOME_DIR:-/workspace/.happy}/daemon.state.json.lock"
|
||||
|
||||
cd "$WORKSPACE_DIR"
|
||||
happy daemon start || echo "Happy Coder daemon failed to start, continuing anyway..."
|
||||
|
||||
# Create Happy Coder log file
|
||||
HAPPY_LOG="/tmp/happy-coder.log"
|
||||
touch "$HAPPY_LOG"
|
||||
chown claude:claude "$HAPPY_LOG"
|
||||
|
||||
# Start Happy Coder as claude user
|
||||
sudo -u claude bash -c "cd '$WORKSPACE_DIR' && happy-coder > '$HAPPY_LOG' 2>&1 &"
|
||||
|
||||
# Save PID for monitoring
|
||||
echo $! > /tmp/happy-coder.pid
|
||||
|
||||
echo "Happy Coder started (PID: $(cat /tmp/happy-coder.pid))"
|
||||
echo "Logs available at: $HAPPY_LOG"
|
||||
echo "Happy Coder daemon started"
|
||||
|
||||
# Export workspace directory for startapp.sh
|
||||
echo "$WORKSPACE_DIR" > /tmp/workspace-dir
|
||||
|
||||
+1
-1
@@ -18,4 +18,4 @@ echo "Opening Antigravity in: $WORKSPACE_DIR"
|
||||
|
||||
# Start Antigravity (VSCode) in the workspace directory as claude user
|
||||
# The baseimage-gui will handle the GUI display
|
||||
exec sudo -u claude code --new-window --wait "$WORKSPACE_DIR"
|
||||
exec code --new-window --wait "$WORKSPACE_DIR"
|
||||
|
||||
Reference in New Issue
Block a user