From e06d302b8cf8fb80cc6bbd97a24fb7447ade1167 Mon Sep 17 00:00:00 2001 From: Adil Atalay Hamamcioglu <38231748+recoskyler@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:31:46 +0000 Subject: [PATCH 1/6] docs: add host credentials section to README.md with mount details --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index d9daeb9..e4c66d8 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,12 @@ Docker-based dev containers with Claude Code, MCP servers, and common tooling pr - ..:/workspace:cached - home:/home - /var/run/docker.sock:/var/run/docker.sock + # Host credentials (see "Host Credentials" section below) + - ~/.claude:/home/dev/.claude + - ~/.ssh:/home/dev/.ssh:ro + - ~/.gitconfig:/home/dev/.gitconfig:ro + - ~/.config/gh:/home/dev/.config/gh:ro + - ~/.config/github-copilot:/home/dev/.config/github-copilot:ro # Overrides default command so things don't # shut down after the process ends @@ -230,6 +236,25 @@ Secret-dependent MCP servers and ntfy hooks are configured at **runtime** (first > Optional MCP servers and ntfy hooks are only configured when their corresponding environment variables are set. +## Host Credentials + +Mount host directories to forward credentials and settings into the container. The entrypoint automatically merges image-built tooling (plugins, skills, rules, GSD) with your host files — nothing is lost. + +| Host Path | Container Path | Purpose | +|-----------|---------------|---------| +| `~/.claude` | `/home/dev/.claude` | Claude Code auth, settings, memory, and projects | +| `~/.ssh` | `/home/dev/.ssh:ro` | SSH keys for git and remote access | +| `~/.gitconfig` | `/home/dev/.gitconfig:ro` | Git identity and config | +| `~/.config/gh` | `/home/dev/.config/gh:ro` | GitHub CLI auth tokens | +| `~/.config/github-copilot` | `/home/dev/.config/github-copilot:ro` | GitHub Copilot OAuth tokens | +| `~/.gnupg` | `/home/dev/.gnupg:ro` | GPG keys for signed commits | + +> **How the `~/.claude` merge works:** At build time, the image snapshots all tooling to `/opt/devcontainer-claude/`. When a host `~/.claude` bind-mount is detected at container start, the entrypoint copies missing tooling (plugins, skills, rules, GSD) into the mounted directory without overwriting host files. The image's `CLAUDE.md` is appended if the host version lacks the DevContainer section. The merge is idempotent — container restarts won't duplicate content. + +> **Note:** Use `:ro` (read-only) for credentials you don't want the container to modify. `~/.claude` is mounted read-write because Claude Code writes memory, session state, and settings at runtime. + +> **Important:** Ensure host files exist before first launch. If a mounted path doesn't exist on the host, Docker creates it as an empty directory, which may cause unexpected behavior. + ## CI/CD Two GitHub Actions workflows build and verify images: From 2e200341579b7e632b7af381e534ebfaec3d1071 Mon Sep 17 00:00:00 2001 From: Adil Atalay Hamamcioglu <38231748+recoskyler@users.noreply.github.com> Date: Thu, 11 Jun 2026 23:06:19 +0000 Subject: [PATCH 2/6] feat: make Docker (DinD/DoD) optional via INSTALL_DOCKER build arg Add INSTALL_DOCKER build arg (default true) to the base image so the Docker CLI + Compose plugin (Docker-outside-of-Docker, via a bind-mounted host socket) can be omitted entirely. The arg lives on the base and propagates to every variant through FROM. CI now builds every image in two flavors via a doubled matrix in both build.yml and check.yml: - base job runs 2 matrix jobs (docker + no-docker) - variants job runs 10 (5 variants x {docker, no-docker}) - no-docker images publish with a -nodocker tag suffix (metadata-action flavor suffix, onlatest=true so :latest-nodocker never clobbers :latest) - parallel -nodocker GHA cache scopes (base-nodocker, bun-nodocker, ...) - flutter no-docker chain preserved: no-docker base -> VNC -> flutter Docs (README, CLAUDE.md, devcontainer-claude.md) updated: Images table lists both flavors, Build Arguments documents INSTALL_DOCKER, CI/CD notes reflect the doubled matrix and new cache scopes. Also removes unused devcontainer tooling that was already staged on this branch: Context7 MCP + suggestion hook, gstack skills, and the ~/.claude home-merge mechanism (merge-claude-home.sh). --- .github/workflows/build.yml | 78 ++++++++++++++++++++++++++++-- .github/workflows/check.yml | 76 +++++++++++++++++++++++++++-- .planning/research/FEATURES.md | 2 +- CLAUDE.md | 4 +- README.md | 57 ++++++++-------------- base/Dockerfile | 30 +++++++----- base/devcontainer-claude.md | 27 +---------- scripts/init-claude-mcp.sh | 50 ------------------- scripts/merge-claude-home.sh | 67 -------------------------- scripts/setup-claude.sh | 82 +++----------------------------- scripts/suggest-context7-hook.sh | 6 --- scripts/verify-tools.sh | 7 --- 12 files changed, 194 insertions(+), 292 deletions(-) delete mode 100644 scripts/merge-claude-home.sh delete mode 100644 scripts/suggest-context7-hook.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a064ced..5202e8a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,9 +17,20 @@ jobs: base: runs-on: ubuntu-latest timeout-minutes: 30 + name: devcontainer-base${{ matrix.suffix }} permissions: contents: read packages: write + strategy: + fail-fast: false + matrix: + include: + - install_docker: true + suffix: "" + scope: base + - install_docker: false + suffix: "-nodocker" + scope: base-nodocker steps: - name: Checkout @@ -40,6 +51,8 @@ jobs: uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/devcontainer-base + flavor: | + suffix=${{ matrix.suffix }},onlatest=true tags: | type=raw,value=latest,enable={{is_default_branch}} type=semver,pattern={{version}} @@ -53,13 +66,16 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=base - cache-to: type=gha,mode=max,scope=base + build-args: | + INSTALL_DOCKER=${{ matrix.install_docker }} + cache-from: type=gha,scope=${{ matrix.scope }} + cache-to: type=gha,mode=max,scope=${{ matrix.scope }} # ── Phase 2: Build and push language images (parallel) ────────────────────── variants: needs: base runs-on: ubuntu-latest + name: ${{ matrix.name }}${{ matrix.suffix }} timeout-minutes: 30 permissions: contents: read @@ -71,19 +87,67 @@ jobs: include: - name: trixie-bun-nvm-uv-claude scope: bun + base_scope: base + install_docker: true + suffix: "" + build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest + - name: trixie-bun-nvm-uv-claude + scope: bun-nodocker + base_scope: base-nodocker + install_docker: false + suffix: "-nodocker" build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest - name: trixie-php-nvm-uv-claude scope: php + base_scope: base + install_docker: true + suffix: "" + build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest + - name: trixie-php-nvm-uv-claude + scope: php-nodocker + base_scope: base-nodocker + install_docker: false + suffix: "-nodocker" build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest - name: trixie-rust-nvm-uv-claude scope: rust + base_scope: base + install_docker: true + suffix: "" + build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest + - name: trixie-rust-nvm-uv-claude + scope: rust-nodocker + base_scope: base-nodocker + install_docker: false + suffix: "-nodocker" build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest - name: trixie-vnc-nvm-uv-claude scope: vnc + base_scope: base + install_docker: true + suffix: "" + build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest + - name: trixie-vnc-nvm-uv-claude + scope: vnc-nodocker + base_scope: base-nodocker + install_docker: false + suffix: "-nodocker" build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest - name: trixie-vnc-flutter-rust-nvm-uv-claude scope: flutter + base_scope: base + install_docker: true + suffix: "" + needs_vnc: true + vnc_scope: vnc + build_contexts: trixie-vnc-nvm-uv-claude:latest=docker-image://localhost:5000/trixie-vnc-nvm-uv-claude:latest + - name: trixie-vnc-flutter-rust-nvm-uv-claude + scope: flutter-nodocker + base_scope: base-nodocker + install_docker: false + suffix: "-nodocker" needs_vnc: true + vnc_scope: vnc-nodocker build_contexts: trixie-vnc-nvm-uv-claude:latest=docker-image://localhost:5000/trixie-vnc-nvm-uv-claude:latest services: @@ -118,7 +182,9 @@ jobs: file: base/Dockerfile push: true tags: localhost:5000/devcontainer-base:latest - cache-from: type=gha,scope=base + build-args: | + INSTALL_DOCKER=${{ matrix.install_docker }} + cache-from: type=gha,scope=${{ matrix.base_scope }} - name: Rebuild VNC from cache and push to local registry if: matrix.needs_vnc @@ -129,13 +195,17 @@ jobs: push: true tags: localhost:5000/trixie-vnc-nvm-uv-claude:latest build-contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest - cache-from: type=gha,scope=vnc + build-args: | + INSTALL_DOCKER=${{ matrix.install_docker }} + cache-from: type=gha,scope=${{ matrix.vnc_scope }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.name }} + flavor: | + suffix=${{ matrix.suffix }},onlatest=true tags: | type=raw,value=latest,enable={{is_default_branch}} type=semver,pattern={{version}} diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 32ac2d9..487c22b 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -13,9 +13,20 @@ jobs: base: runs-on: ubuntu-latest timeout-minutes: 30 + name: devcontainer-base${{ matrix.suffix }} permissions: contents: read pull-requests: write + strategy: + fail-fast: false + matrix: + include: + - install_docker: true + suffix: "" + scope: base + - install_docker: false + suffix: "-nodocker" + scope: base-nodocker steps: - name: Checkout @@ -32,7 +43,9 @@ jobs: push: false load: true tags: devcontainer-base:latest - cache-from: type=gha,scope=base + build-args: | + INSTALL_DOCKER=${{ matrix.install_docker }} + cache-from: type=gha,scope=${{ matrix.scope }} - name: Verify base tools run: | @@ -52,7 +65,7 @@ jobs: ICON="❌" fi { - echo "### ${ICON} Tool Verification: base" + echo "### ${ICON} Tool Verification: base${{ matrix.suffix }}" echo "" echo "
" echo "Click to expand verification output" @@ -69,6 +82,7 @@ jobs: variants: needs: base runs-on: ubuntu-latest + name: ${{ matrix.name }}${{ matrix.suffix }} timeout-minutes: 30 permissions: contents: read @@ -80,19 +94,67 @@ jobs: include: - name: trixie-bun-nvm-uv-claude scope: bun + base_scope: base + install_docker: true + suffix: "" + build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest + - name: trixie-bun-nvm-uv-claude + scope: bun-nodocker + base_scope: base-nodocker + install_docker: false + suffix: "-nodocker" build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest - name: trixie-php-nvm-uv-claude scope: php + base_scope: base + install_docker: true + suffix: "" + build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest + - name: trixie-php-nvm-uv-claude + scope: php-nodocker + base_scope: base-nodocker + install_docker: false + suffix: "-nodocker" build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest - name: trixie-rust-nvm-uv-claude scope: rust + base_scope: base + install_docker: true + suffix: "" + build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest + - name: trixie-rust-nvm-uv-claude + scope: rust-nodocker + base_scope: base-nodocker + install_docker: false + suffix: "-nodocker" build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest - name: trixie-vnc-nvm-uv-claude scope: vnc + base_scope: base + install_docker: true + suffix: "" + build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest + - name: trixie-vnc-nvm-uv-claude + scope: vnc-nodocker + base_scope: base-nodocker + install_docker: false + suffix: "-nodocker" build_contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest - name: trixie-vnc-flutter-rust-nvm-uv-claude scope: flutter + base_scope: base + install_docker: true + suffix: "" + needs_vnc: true + vnc_scope: vnc + build_contexts: trixie-vnc-nvm-uv-claude:latest=docker-image://localhost:5000/trixie-vnc-nvm-uv-claude:latest + - name: trixie-vnc-flutter-rust-nvm-uv-claude + scope: flutter-nodocker + base_scope: base-nodocker + install_docker: false + suffix: "-nodocker" needs_vnc: true + vnc_scope: vnc-nodocker build_contexts: trixie-vnc-nvm-uv-claude:latest=docker-image://localhost:5000/trixie-vnc-nvm-uv-claude:latest services: @@ -120,7 +182,9 @@ jobs: file: base/Dockerfile push: true tags: localhost:5000/devcontainer-base:latest - cache-from: type=gha,scope=base + build-args: | + INSTALL_DOCKER=${{ matrix.install_docker }} + cache-from: type=gha,scope=${{ matrix.base_scope }} - name: Rebuild VNC from cache and push to local registry if: matrix.needs_vnc @@ -131,7 +195,9 @@ jobs: push: true tags: localhost:5000/trixie-vnc-nvm-uv-claude:latest build-contexts: devcontainer-base:latest=docker-image://localhost:5000/devcontainer-base:latest - cache-from: type=gha,scope=vnc + build-args: | + INSTALL_DOCKER=${{ matrix.install_docker }} + cache-from: type=gha,scope=${{ matrix.vnc_scope }} - name: Build ${{ matrix.name }} uses: docker/build-push-action@v6 @@ -156,7 +222,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - VARIANT="${{ matrix.name }}" + VARIANT="${{ matrix.name }}${{ matrix.suffix }}" if grep -q "ALL_CHECKS_PASSED" /tmp/verify-output.txt 2>/dev/null; then ICON="✅" else diff --git a/.planning/research/FEATURES.md b/.planning/research/FEATURES.md index 42de19a..9b9a813 100644 --- a/.planning/research/FEATURES.md +++ b/.planning/research/FEATURES.md @@ -34,7 +34,7 @@ Features that set the product apart. Not required, but valuable. | Full Android Studio IDE via VNC | Most Flutter Docker images omit the IDE; having it lets developers use the full GUI (layout inspector, profiler, device manager) over VNC | HIGH | ~1 GB download. Silent install from tarball. Launchable via VNC. Most competitors only provide cmdline-tools. Adds significant image size but huge DX value. | | Pre-created AVD device profile | Zero-config emulator start -- `emulator -avd dev_device` works immediately | LOW | `avdmanager create avd -n dev_device -k "system-images;android-35;google_apis;x86_64" -d pixel_7` during build. Saves developers 5-10 min of first-time setup. | | Flutter + Rust FFI ready (cargo + Dart FFI) | Combined stack enables flutter_rust_bridge / rinf workflows out of the box; rare in DevContainer images | LOW | Both Rust and Flutter installed. No extra tooling needed beyond what's already table stakes. Documenting the capability in CLAUDE.md is the differentiator. | -| Claude Code + MCP servers pre-configured | AI-assisted development with semantic code analysis, context7 docs, memory -- unique to this project | LOW | Inherited from base image. Already configured with serena, context7, memory MCPs. No competitors have this. | +| Claude Code + MCP servers pre-configured | AI-assisted development with semantic code analysis, memory -- unique to this project | LOW | Inherited from base image. Already configured with memory MCPs. No competitors have this. | | Chromium for Flutter web development | Enables `flutter run -d chrome` and `flutter test --platform chrome` inside the container | MEDIUM | `apt-get install chromium`. Set `CHROME_EXECUTABLE=/usr/bin/chromium`. Enables web target alongside Android without leaving the container. | | Configurable Android API level via build arg | Different projects target different Android versions; build arg makes image flexible | LOW | `ARG ANDROID_API=35` controlling which platform/system-image/build-tools to install. Avoids maintaining multiple images. | | Gradle + pub cache volume mount guidance | Dramatically faster rebuilds; avoids re-downloading 500+ MB of dependencies per project | LOW | Not baked into image, but documented in devcontainer.json examples. Mount `~/.gradle` and `~/.pub-cache` as named volumes. | diff --git a/CLAUDE.md b/CLAUDE.md index 409f968..1436618 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,11 +40,11 @@ This is a project containing pre-configured Dockerfiles for building and hosting ## CI/CD notes -- Both workflows use a two-job structure: `base` builds the base image with GHA cache, then `variants` runs 5 parallel matrix jobs +- Both workflows use a two-job structure: `base` builds the base image with GHA cache, then `variants` runs the variant matrix jobs. Every image is built twice (Docker + no-docker) via the `INSTALL_DOCKER` build-arg: `base` runs 2 matrix jobs and `variants` runs 10 (5 × {docker, no-docker}); no-docker builds get a `-nodocker` tag suffix - Variant jobs use a `registry:2` service container + `build-contexts` (via `matrix.build_contexts`) to remap FROM images to the local registry — no Dockerfile changes needed - The flutter variant has a three-tier chain (base -> VNC -> flutter) with a conditional VNC rebuild step (`if: matrix.needs_vnc`) - `build.yml` writes GHA cache (`cache-to`); `check.yml` only reads it (`cache-from`) -- GHA cache scopes: `base`, `bun`, `php`, `rust`, `vnc`, `flutter` +- GHA cache scopes: `base`, `bun`, `php`, `rust`, `vnc`, `flutter` (plus `base-nodocker`, `bun-nodocker`, `php-nodocker`, `rust-nodocker`, `vnc-nodocker`, `flutter-nodocker` for the no-docker builds) ## Testing and validation diff --git a/README.md b/README.md index e4c66d8..309a0c5 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,11 @@ Docker-based dev containers with Claude Code, MCP servers, and common tooling pr - default environment: - - CONTEXT7_API_KEY=your-key - AUTOMEM_ENDPOINT=your-endpoint - AUTOMEM_API_KEY=your-key - NTFY_URL=https://ntfy.sh/your-topic - NTFY_TOKEN=your-token - ENABLE_TOOL_SEARCH=true - - ENABLE_EXPERIMENTAL_MCP_CLI=false - CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS="1" - DISPLAY=":0" @@ -149,14 +147,16 @@ Docker-based dev containers with Claude Code, MCP servers, and common tooling pr All images extend a shared base (`base/Dockerfile` — `debian:trixie`) and run as user `dev` (UID 1000, home `/home/dev`). -| Image | Extra stack | -|-------|-------------| -| `ghcr.io/recoskyler/devcontainer-base:latest` | — (base only) | -| `ghcr.io/recoskyler/trixie-bun-nvm-uv-claude:latest` | Bun | -| `ghcr.io/recoskyler/trixie-php-nvm-uv-claude:latest` | PHP 8.4, Composer | -| `ghcr.io/recoskyler/trixie-rust-nvm-uv-claude:latest` | Rust toolchain | -| `ghcr.io/recoskyler/trixie-vnc-nvm-uv-claude:latest` | x11vnc, Xvfb | -| `ghcr.io/recoskyler/trixie-vnc-flutter-rust-nvm-uv-claude:latest` | Flutter, Rust, Android SDK, VNC | +Every image is published in two flavors: the default tag includes the Docker CLI + Compose plugin, and the `-nodocker` tag omits them (built with `INSTALL_DOCKER=false`). + +| Image | Docker tag | No-docker tag | Extra stack | +|-------|------------|---------------|-------------| +| `ghcr.io/recoskyler/devcontainer-base` | `:latest` | `:latest-nodocker` | — (base only) | +| `ghcr.io/recoskyler/trixie-bun-nvm-uv-claude` | `:latest` | `:latest-nodocker` | Bun | +| `ghcr.io/recoskyler/trixie-php-nvm-uv-claude` | `:latest` | `:latest-nodocker` | PHP 8.4, Composer | +| `ghcr.io/recoskyler/trixie-rust-nvm-uv-claude` | `:latest` | `:latest-nodocker` | Rust toolchain | +| `ghcr.io/recoskyler/trixie-vnc-nvm-uv-claude` | `:latest` | `:latest-nodocker` | x11vnc, Xvfb | +| `ghcr.io/recoskyler/trixie-vnc-flutter-rust-nvm-uv-claude` | `:latest` | `:latest-nodocker` | Flutter, Rust, Android SDK, VNC | ## What's Included @@ -165,12 +165,11 @@ All images extend a shared base (`base/Dockerfile` — `debian:trixie`) and run - **Node.js** via NVM (default: 24.12.0) - **UV** (Python package manager) - **Claude Code** CLI + plugins (ECC, Superpowers, official plugin suite) -- **MCP servers**: Serena, Context7, Automem -- **GSD** (Get Shit Done for Claude Code) +- **MCP servers**: Automem +- **GSD** (Git Ship Done Core + Browser) - **Agent Browser** + Chrome -- **gstack** — 28 specialized Claude Code engineering skills ([garrytan/gstack](https://github.com/garrytan/gstack)) - **Bun** runtime (`bun`, `bunx`) -- **Docker** CLI + Compose plugin (`docker`, `docker compose`) — mount the host socket to use; works without `sudo` (the entrypoint automatically matches the socket's GID) +- **Docker** CLI + Compose plugin (`docker`, `docker compose`) — mount the host socket to use; works without `sudo` (the entrypoint automatically matches the socket's GID). Optional: build with `--build-arg INSTALL_DOCKER=false` to omit it (see [Build Arguments](#build-arguments)) - **CLI tools**: git, curl, wget, vim, nano, jq, tmux, xclip, openssh-client, gnupg, cmake, less, unzip, gh, pnpm, tsx - **Search & file tools**: ripgrep, fd-find, fzf, bat, tree - **PDF tools**: poppler-utils (pdftotext, pdfinfo, etc.) @@ -221,6 +220,9 @@ Extends the VNC image with Flutter, Rust, and Android tooling. | Argument | Default | Description | |----------|---------|-------------| | `NODE_VERSION` | `24.12.0` | Node.js version installed via NVM | +| `INSTALL_DOCKER` | `true` | Install the Docker CLI + Compose plugin (Docker-outside-of-Docker). Set to `false` to omit them. | + +> `INSTALL_DOCKER` is defined on the base image, so it applies to every variant. Pass it when building the base (variants inherit the result via their `FROM`). When set to `false`, no Docker CLI is installed; the `docker` group and socket-fix entrypoint remain but are inert unless a host socket is mounted. CI publishes both flavors for every image: a Docker-enabled image (`:latest`) and a no-docker image (`:latest-nodocker`). ## Runtime Environment Variables @@ -228,7 +230,6 @@ Secret-dependent MCP servers and ntfy hooks are configured at **runtime** (first | Variable | Description | |----------|-------------| -| `CONTEXT7_API_KEY` | [Context7](https://context7.com) MCP server API key (skipped if empty) | | `AUTOMEM_ENDPOINT` | [Automem](https://github.com/verygoodplugins/mcp-automem) MCP server endpoint URL (skipped if empty) | | `AUTOMEM_API_KEY` | [Automem](https://github.com/verygoodplugins/mcp-automem) MCP server API key (skipped if empty) | | `NTFY_URL` | [ntfy](https://ntfy.sh) server/topic URL for notification hooks (skipped if empty) | @@ -236,35 +237,18 @@ Secret-dependent MCP servers and ntfy hooks are configured at **runtime** (first > Optional MCP servers and ntfy hooks are only configured when their corresponding environment variables are set. -## Host Credentials - -Mount host directories to forward credentials and settings into the container. The entrypoint automatically merges image-built tooling (plugins, skills, rules, GSD) with your host files — nothing is lost. - -| Host Path | Container Path | Purpose | -|-----------|---------------|---------| -| `~/.claude` | `/home/dev/.claude` | Claude Code auth, settings, memory, and projects | -| `~/.ssh` | `/home/dev/.ssh:ro` | SSH keys for git and remote access | -| `~/.gitconfig` | `/home/dev/.gitconfig:ro` | Git identity and config | -| `~/.config/gh` | `/home/dev/.config/gh:ro` | GitHub CLI auth tokens | -| `~/.config/github-copilot` | `/home/dev/.config/github-copilot:ro` | GitHub Copilot OAuth tokens | -| `~/.gnupg` | `/home/dev/.gnupg:ro` | GPG keys for signed commits | - -> **How the `~/.claude` merge works:** At build time, the image snapshots all tooling to `/opt/devcontainer-claude/`. When a host `~/.claude` bind-mount is detected at container start, the entrypoint copies missing tooling (plugins, skills, rules, GSD) into the mounted directory without overwriting host files. The image's `CLAUDE.md` is appended if the host version lacks the DevContainer section. The merge is idempotent — container restarts won't duplicate content. - -> **Note:** Use `:ro` (read-only) for credentials you don't want the container to modify. `~/.claude` is mounted read-write because Claude Code writes memory, session state, and settings at runtime. - -> **Important:** Ensure host files exist before first launch. If a mounted path doesn't exist on the host, Docker creates it as an empty directory, which may cause unexpected behavior. - ## CI/CD Two GitHub Actions workflows build and verify images: -- **`build.yml`** — Runs on push to `latest` or version tags. Builds the base image with GHA cache, then builds and pushes all 5 variants to GHCR in parallel (matrix strategy). +- **`build.yml`** — Runs on push to `latest` or version tags. Builds the base image with GHA cache, then builds and pushes all variants to GHCR in parallel (matrix strategy). - **`check.yml`** — Runs on PRs to `latest`. Same structure but read-only cache (no `cache-to`) and no push to GHCR. Each variant runs tool verification and posts results as PR comments. +Each workflow builds every image **twice** via the matrix — once with Docker (the default) and once without (`INSTALL_DOCKER=false`). The `base` job runs 2 matrix jobs (Docker + no-docker) and the `variants` job runs 10 (5 variants × {docker, no-docker}). No-docker images carry a `-nodocker` tag suffix. + Both workflows use a local `registry:2` service container and `build-contexts` to remap `FROM` images at build time, requiring zero Dockerfile changes. The flutter variant has a three-tier chain (base → VNC → flutter) with a conditional VNC rebuild step. -Images are published to GHCR at `ghcr.io//`. +Images are published to GHCR at `ghcr.io//`. Docker-enabled images use the normal tags (`:latest`, `:`); no-docker images use the same tags with a `-nodocker` suffix (`:latest-nodocker`, `:-nodocker`). ### Tags @@ -302,7 +286,6 @@ Then run with your API keys as environment variables: ```bash docker run -it \ - -e CONTEXT7_API_KEY=your-key \ -e AUTOMEM_ENDPOINT=your-endpoint \ -e AUTOMEM_API_KEY=your-key \ -e NTFY_URL=https://ntfy.sh/your-topic \ diff --git a/base/Dockerfile b/base/Dockerfile index 1bae253..65013d4 100644 --- a/base/Dockerfile +++ b/base/Dockerfile @@ -39,6 +39,7 @@ RUN apt-get update \ poppler-utils \ postgresql-client \ python3-pip \ + python3-dev \ redis-tools \ ripgrep \ sudo \ @@ -87,10 +88,14 @@ WORKDIR /workspace RUN curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg --dearmor | tee /usr/share/keyrings/stripe.gpg \ && echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" | tee -a /etc/apt/sources.list.d/stripe.list -# Docker CLI -RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/debian trixie stable" \ - | tee /etc/apt/sources.list.d/docker.list +# Docker CLI (outside-of-Docker: talks to a bind-mounted host socket). +# Set INSTALL_DOCKER=false to omit the Docker CLI and Compose plugin entirely. +ARG INSTALL_DOCKER=true +RUN if [ "$INSTALL_DOCKER" = "true" ]; then \ + curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/debian trixie stable" \ + | tee /etc/apt/sources.list.d/docker.list; \ + fi # GH CLI + Stripe CLI install RUN mkdir -p -m 755 /etc/apt/keyrings \ @@ -101,7 +106,8 @@ RUN mkdir -p -m 755 /etc/apt/keyrings \ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ && apt-get update \ - && apt-get -y install --no-install-recommends gh stripe docker-ce-cli docker-compose-plugin + && if [ "$INSTALL_DOCKER" = "true" ]; then DOCKER_PKGS="docker-ce-cli docker-compose-plugin"; fi \ + && apt-get -y install --no-install-recommends gh stripe $DOCKER_PKGS # ── CLI tools (releases) ──────────────────────────────────────────────────── @@ -111,6 +117,8 @@ RUN ARCH=$(dpkg --print-architecture) \ && dpkg -i /tmp/git-delta.deb \ && rm -f /tmp/git-delta.deb +RUN curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash + # AWS CLI v2 RUN ARCH=$(uname -m) \ && curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-${ARCH}.zip" -o /tmp/awscliv2.zip \ @@ -134,6 +142,9 @@ RUN curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key | gpg --d # ── User setup ─────────────────────────────────────────────────────────────── +# Always create the docker group / membership even when INSTALL_DOCKER=false: +# it is harmless without the CLI and keeps docker-sock-fix.sh working if a host +# socket is later bind-mounted. RUN groupadd -f docker RUN id dev >/dev/null 2>&1 \ @@ -213,23 +224,19 @@ RUN curl -fsSL https://bun.sh/install | bash ENV PATH="/home/dev/.bun/bin:${PATH}" # Claude Plugins, MCP, Hooks -ARG CONTEXT7_API_KEY="" ARG AUTOMEM_ENDPOINT="" ARG AUTOMEM_API_KEY="" ARG NTFY_URL="" ARG NTFY_TOKEN="" -ENV CONTEXT7_API_KEY=${CONTEXT7_API_KEY} ENV AUTOMEM_ENDPOINT=${AUTOMEM_ENDPOINT} ENV AUTOMEM_API_KEY=${AUTOMEM_API_KEY} ENV NTFY_URL=${NTFY_URL} ENV NTFY_TOKEN=${NTFY_TOKEN} COPY --chown=dev:dev scripts/ntfy-hook.sh $HOME/.local/bin/ntfy-hook.sh -COPY --chown=dev:dev scripts/suggest-context7-hook.sh $HOME/.local/bin/suggest-context7-hook.sh COPY --chown=dev:dev scripts/init-claude-mcp.sh $HOME/.local/bin/init-claude-mcp.sh -COPY --chown=dev:dev scripts/merge-claude-home.sh $HOME/.local/bin/merge-claude-home.sh -RUN chmod +x $HOME/.local/bin/ntfy-hook.sh $HOME/.local/bin/suggest-context7-hook.sh $HOME/.local/bin/init-claude-mcp.sh $HOME/.local/bin/merge-claude-home.sh +RUN chmod +x $HOME/.local/bin/ntfy-hook.sh $HOME/.local/bin/init-claude-mcp.sh COPY --chown=dev:dev scripts/setup-claude.sh /tmp/setup-claude.sh RUN bash /tmp/setup-claude.sh && rm -f /tmp/setup-claude.sh @@ -258,7 +265,8 @@ RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers # agent-browser WORKDIR /workspace -RUN npm install -y -g agent-browser clideck @mariozechner/pi-coding-agent +RUN npm install -y -g --ignore-scripts @earendil-works/pi-coding-agent +RUN npm install -y -g agent-browser @opengsd/gsd-browser RUN agent-browser install --with-deps diff --git a/base/devcontainer-claude.md b/base/devcontainer-claude.md index 87fd4ac..cf05766 100644 --- a/base/devcontainer-claude.md +++ b/base/devcontainer-claude.md @@ -11,7 +11,7 @@ This is a DevContainer running Debian Trixie. You are the `dev` user with passwo ### CLI Tools - `git`, `gh` (GitHub CLI), `delta` (git-delta) -- `docker`, `docker compose` (Docker CLI + Compose plugin) +- `docker`, `docker compose` (Docker CLI + Compose plugin — omitted when built with `INSTALL_DOCKER=false`) - `aws` (AWS CLI v2), `terraform`, `kubectl`, `stripe` - `claude` (Claude Code CLI) - `brew` (Homebrew — used for select packages, on `$PATH` via `/home/linuxbrew/.linuxbrew/bin`) @@ -41,30 +41,6 @@ This is a DevContainer running Debian Trixie. You are the `dev` user with passwo ## Working directory Default: `/workspace` -## MCP Servers - -MCP servers are configured on first shell login via `init-claude-mcp.sh`. Optional servers are only added when their env vars are set. - -- `serena` — semantic code analysis (symbols, references, overview) -- `context7` — library documentation lookup (if CONTEXT7_API_KEY is set) -- `memory` — persistent memory via Automem (if AUTOMEM_ENDPOINT is set) - -## Plugins & Skills -- GSD (`/gsd:*`) — project management and execution workflow -- gstack — 28 specialized engineering skills as slash commands (garrytan/gstack) -- superpowers — brainstorming, TDD, debugging, code review skills -- everything-claude-code (ECC) — rules installed at `~/.claude/rules/` -- feature-dev, frontend-design, code-review, commit-commands, pr-review-toolkit -- hookify, playground, claude-md-management, claude-code-setup -- ralph-loop, security-guidance -- explanatory-output-style, learning-output-style -- typescript-lsp, pyright-lsp, php-lsp, laravel-boost -- agent-browser skill at `~/.claude/skills/agent-browser/SKILL.md` - -## Hooks -- Context7 suggestion hook — suggests context7 when WebSearch is used (if configured) -- Ntfy notification hook — sends push notifications on task completion (if configured) - ## Browser Automation Use `agent-browser` for web automation. Run `agent-browser --help` for all commands. @@ -78,4 +54,3 @@ Core workflow: ## Notes - Shell is bash. `/bin/sh` is symlinked to `/bin/bash`. - Passwordless sudo is available via `sudo`. -- When host `~/.claude` is bind-mounted, the entrypoint auto-merges image tooling (plugins, skills, rules, GSD) into the mounted directory. Host files (credentials, settings) are never overwritten. diff --git a/scripts/init-claude-mcp.sh b/scripts/init-claude-mcp.sh index f2851b5..ec1dad6 100644 --- a/scripts/init-claude-mcp.sh +++ b/scripts/init-claude-mcp.sh @@ -9,29 +9,6 @@ CLAUDE_JSON="$HOME/.claude.json" [ -f "$CLAUDE_JSON" ] || echo '{}' > "$CLAUDE_JSON" -# --- Context7 MCP --- -if [ -n "$CONTEXT7_API_KEY" ]; then - if ! grep -q '"context7"' "$CLAUDE_JSON" 2>/dev/null; then - $CLAUDE mcp add --transport stdio -s user context7 -- \ - npx -y @upstash/context7-mcp --api-key "$CONTEXT7_API_KEY" - fi - - # Register WebSearch suggestion hook - if ! grep -q 'suggest-context7-hook' "$CLAUDE_JSON" 2>/dev/null; then - HOOK_CMD="$HOME/.local/bin/suggest-context7-hook.sh" - chmod +x "$HOOK_CMD" 2>/dev/null - - HOOKS_JSON=$(jq -n \ - --arg cmd "$HOOK_CMD" \ - '{ - PreToolUse: [{matcher: "WebSearch", hooks: [{type: "command", command: $cmd}]}] - }') - - jq --argjson newhooks "$HOOKS_JSON" '. + {hooks: ((.hooks // {}) * $newhooks)}' "$CLAUDE_JSON" > "$CLAUDE_JSON.tmp" - mv "$CLAUDE_JSON.tmp" "$CLAUDE_JSON" - fi -fi - # --- Automem MCP --- if [ -n "$AUTOMEM_ENDPOINT" ] && [ -n "$AUTOMEM_API_KEY" ]; then if ! grep -q '"memory"' "$CLAUDE_JSON" 2>/dev/null; then @@ -61,31 +38,4 @@ if [ -n "$NTFY_URL" ] && [ -n "$NTFY_TOKEN" ]; then fi fi -# --- Serena user-scope config --- -# Exclude Serena memory tools via user config -SERENA_USER="$HOME/.serena/user.yml" -mkdir -p "$HOME/.serena" -if [ ! -f "$SERENA_USER" ] || grep -q '^excluded_tools: \[\]' "$SERENA_USER"; then - if [ -n "$AUTOMEM_ENDPOINT" ] && [ -n "$AUTOMEM_API_KEY" ]; then - # Automem replaces all Serena memory/onboarding tools - TOOLS=(write_memory read_memory edit_memory delete_memory list_memories check_onboarding_performed onboarding) - else - # No Automem backend: disable memory mutation tools - TOOLS=(write_memory delete_memory read_memory) - fi - - YAML="excluded_tools:" - for t in "${TOOLS[@]}"; do - YAML="${YAML} -- ${t}" - done - - if [ -f "$SERENA_USER" ]; then - awk -v new="$YAML" '/^excluded_tools: \[\]/ { print new; next } 1' "$SERENA_USER" > "$SERENA_USER.tmp" - mv "$SERENA_USER.tmp" "$SERENA_USER" - else - printf '%s\n' "$YAML" > "$SERENA_USER" - fi -fi - touch /tmp/.claude-mcp-init diff --git a/scripts/merge-claude-home.sh b/scripts/merge-claude-home.sh deleted file mode 100644 index 1b6bf16..0000000 --- a/scripts/merge-claude-home.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash -set -e - -# Merge image-built ~/.claude tooling into a host-mounted ~/.claude directory. -# Called by the entrypoint (docker-sock-fix.sh) at container start. -# -# Three scenarios: -# 1. No backup exists (/opt/devcontainer-claude missing) -> exit 0 -# 2. No mount detected (image marker matches) -> exit 0 -# 3. Host mount detected -> merge image tooling additively, skip host files - -BACKUP="/opt/devcontainer-claude" -LIVE="$HOME/.claude" - -# If backup doesn't exist, nothing to merge (image not built with snapshot) -if [ ! -d "$BACKUP" ]; then - exit 0 -fi - -# If the live directory has the same image marker as the backup, no mount happened -if [ -f "$LIVE/.image-marker" ] && [ -f "$BACKUP/.image-marker" ]; then - if [ "$(cat "$LIVE/.image-marker")" = "$(cat "$BACKUP/.image-marker")" ]; then - exit 0 - fi -fi - -# Idempotency: if we already merged for this backup version, skip -BACKUP_HASH="" -if [ -f "$BACKUP/.image-marker" ]; then - BACKUP_HASH=$(cat "$BACKUP/.image-marker") -fi - -if [ -f "$LIVE/.merge-done" ] && [ "$(cat "$LIVE/.merge-done")" = "$BACKUP_HASH" ]; then - exit 0 -fi - -# Ensure live directory exists -mkdir -p "$LIVE" - -# Merge directories — cp -rn (no-clobber) so host files are never overwritten -for dir in plugins skills rules get-shit-done agents; do - if [ -d "$BACKUP/$dir" ]; then - if [ ! -d "$LIVE/$dir" ]; then - cp -r "$BACKUP/$dir" "$LIVE/$dir" - else - cp -rn "$BACKUP/$dir/." "$LIVE/$dir/" 2>/dev/null || true - fi - fi -done - -# Merge CLAUDE.md — special append logic -if [ -f "$BACKUP/CLAUDE.md" ]; then - if [ ! -f "$LIVE/CLAUDE.md" ]; then - # No host CLAUDE.md — copy from backup - cp "$BACKUP/CLAUDE.md" "$LIVE/CLAUDE.md" - elif ! grep -q '# DevContainer Environment' "$LIVE/CLAUDE.md" 2>/dev/null; then - # Host CLAUDE.md exists but lacks DevContainer section — append - printf '\n\n# --- DevContainer Image (auto-merged) ---\n\n' >>"$LIVE/CLAUDE.md" - cat "$BACKUP/CLAUDE.md" >>"$LIVE/CLAUDE.md" - fi - # If already contains '# DevContainer Environment', skip (already merged or image-native) -fi - -# Write idempotency marker -echo "$BACKUP_HASH" >"$LIVE/.merge-done" - -exit 0 diff --git a/scripts/setup-claude.sh b/scripts/setup-claude.sh index 98e62a3..0ac7a83 100755 --- a/scripts/setup-claude.sh +++ b/scripts/setup-claude.sh @@ -6,92 +6,22 @@ CLAUDE="$HOME/.local/bin/claude" # --- Claude Plugins --- if [ -x "$CLAUDE" ]; then - $CLAUDE plugin marketplace add affaan-m/everything-claude-code - $CLAUDE plugin install everything-claude-code@everything-claude-code - - $CLAUDE plugin marketplace add obra/superpowers - $CLAUDE plugin install superpowers@superpowers-dev + $CLAUDE plugin marketplace add pcvelz/superpowers + $CLAUDE plugin install superpowers-extended-cc@superpowers-extended-cc-marketplace $CLAUDE plugin marketplace add anthropics/claude-plugins-official - $CLAUDE plugin install code-review@claude-plugins-official - $CLAUDE plugin install commit-commands@claude-plugins-official - $CLAUDE plugin install explanatory-output-style@claude-plugins-official - $CLAUDE plugin install hookify@claude-plugins-official - $CLAUDE plugin install feature-dev@claude-plugins-official - $CLAUDE plugin install frontend-design@claude-plugins-official - $CLAUDE plugin install learning-output-style@claude-plugins-official - $CLAUDE plugin install ralph-loop@claude-plugins-official - $CLAUDE plugin install pr-review-toolkit@claude-plugins-official - $CLAUDE plugin install security-guidance@claude-plugins-official - $CLAUDE plugin install claude-md-management@claude-plugins-official - $CLAUDE plugin install claude-code-setup@claude-plugins-official - $CLAUDE plugin install playground@claude-plugins-official - $CLAUDE plugin install typescript-lsp@claude-plugins-official - $CLAUDE plugin install pyright-lsp@claude-plugins-official - $CLAUDE plugin install php-lsp@claude-plugins-official - $CLAUDE plugin install laravel-boost@claude-plugins-official else echo "WARNING: Claude CLI not found at $CLAUDE — skipping plugin setup" fi # --- Agent Browser --- -mkdir -p /home/dev/.claude/skills/agent-browser - -curl -o /home/dev/.claude/skills/agent-browser/SKILL.md https://raw.githubusercontent.com/vercel-labs/agent-browser/main/skills/agent-browser/SKILL.md - -# --- Hookify Fix --- - -HOOKIFY_DIR="$HOME/.claude/plugins/cache/claude-code-plugins/hookify/0.1.0" -[ -d "$HOOKIFY_DIR" ] && ln -sf . "$HOOKIFY_DIR/hookify" - -# --- ECC Rules --- - -git clone https://github.com/affaan-m/everything-claude-code.git /tmp/everything-claude-code -mkdir -p "$HOME/.claude/rules" -cp -r /tmp/everything-claude-code/rules/common/* "$HOME/.claude/rules/" -cp -r /tmp/everything-claude-code/rules/typescript/* "$HOME/.claude/rules/" -rm -rf /tmp/everything-claude-code +npx -y skills add -y --global --all vercel-labs/agent-browser # --- GSD --- -npx -y get-shit-done-cc --claude --global - -# --- gstack --- -# Manual setup (skips Chromium launch check — agent-browser already provides Playwright) - -git clone https://github.com/garrytan/gstack.git "$HOME/.claude/skills/gstack" -cd "$HOME/.claude/skills/gstack" -bun install -bun run build -mkdir -p "$HOME/.gstack/projects" - -# Register skills — symlink each skill subdir into the skills parent -for skill_dir in "$HOME/.claude/skills/gstack"/*/; do - if [ -f "$skill_dir/SKILL.md" ]; then - skill_name="$(basename "$skill_dir")" - [ "$skill_name" = "node_modules" ] && continue - ln -snf "gstack/$skill_name" "$HOME/.claude/skills/$skill_name" - fi -done - -cd /workspace - -# --- MCP Servers --- - -if [ -x "$CLAUDE" ]; then - $CLAUDE mcp add --transport stdio -s user serena -- \ - uvx --from git+https://github.com/oraios/serena serena start-mcp-server --context=claude-code --project-from-cwd -else - echo "WARNING: Claude CLI not found — skipping MCP server setup" -fi +npx -y @opengsd/gsd-core@latest --claude --global -# --- Enable Remote Control for all sessions --- +# --- Headroom --- -# CLAUDE_JSON="$HOME/.claude.json" -# if [ -f "$CLAUDE_JSON" ] && command -v jq >/dev/null 2>&1; then -# jq '. + {"remoteControlAtStartup": true}' "$CLAUDE_JSON" > "$CLAUDE_JSON.tmp" \ -# && mv "$CLAUDE_JSON.tmp" "$CLAUDE_JSON" -# else -# printf '{"remoteControlAtStartup":true}\n' > "$CLAUDE_JSON" -# fi +uv tool install "headroom-ai[all]" diff --git a/scripts/suggest-context7-hook.sh b/scripts/suggest-context7-hook.sh deleted file mode 100644 index 6e30613..0000000 --- a/scripts/suggest-context7-hook.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -# PreToolUse hook for WebSearch: force-suggests using Context7 MCP for documentation queries. -# Registered by init-claude-mcp.sh when CONTEXT7_API_KEY is set. - -echo "IMPORTANT: Context7 MCP is configured and available. For ANY queries about programming libraries, frameworks, APIs, or documentation, you MUST use mcp__context7__resolve-library-id followed by mcp__context7__query-docs INSTEAD of WebSearch. Reserve WebSearch for non-documentation needs only (current events, news, general knowledge). A MCP for the programming library, framework, API, or documentation you are searching for might also be configured and available. Discover the MCPs to check, and try the thing-specific MCP before context7 MCP." -exit 0 diff --git a/scripts/verify-tools.sh b/scripts/verify-tools.sh index 0649cb0..4a378a3 100755 --- a/scripts/verify-tools.sh +++ b/scripts/verify-tools.sh @@ -127,13 +127,6 @@ verify_base() { for cmd in ttyd delta agent-browser claude pi clideck; do check_cmd "$cmd" done - - # Claude ecosystem - check_file "GSD directory" "/home/dev/.claude/get-shit-done" - - for script in ntfy-hook.sh suggest-context7-hook.sh init-claude-mcp.sh; do - check_file "$script" "$HOME/.local/bin/$script" - done } verify_bun() { From 10663a6031e4739ce7e03cb30cf83da1b0997beb Mon Sep 17 00:00:00 2001 From: Adil Atalay Hamamcioglu <38231748+recoskyler@users.noreply.github.com> Date: Thu, 11 Jun 2026 23:31:16 +0000 Subject: [PATCH 3/6] refactor: remove clidec verification --- scripts/verify-tools.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/verify-tools.sh b/scripts/verify-tools.sh index 4a378a3..86af6f1 100755 --- a/scripts/verify-tools.sh +++ b/scripts/verify-tools.sh @@ -124,7 +124,7 @@ verify_base() { done # Other - for cmd in ttyd delta agent-browser claude pi clideck; do + for cmd in ttyd delta agent-browser claude pi; do check_cmd "$cmd" done } From 99e78418b66fdc4ce5cdffd1a84485d52199a8a4 Mon Sep 17 00:00:00 2001 From: Adil Atalay Hamamcioglu <38231748+recoskyler@users.noreply.github.com> Date: Thu, 11 Jun 2026 23:55:20 +0000 Subject: [PATCH 4/6] fix: add storage cleanup step --- .github/workflows/build.yml | 10 ++++++++++ .github/workflows/check.yml | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5202e8a..cf495c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -160,6 +160,16 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Free disk space + run: | + echo "Disk space before cleanup:" + df -h / + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ + /usr/local/share/boost /opt/hostedtoolcache/CodeQL + sudo docker image prune --all --force >/dev/null 2>&1 || true + echo "Disk space after cleanup:" + df -h / + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 487c22b..78a648d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -167,6 +167,16 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Free disk space + run: | + echo "Disk space before cleanup:" + df -h / + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ + /usr/local/share/boost /opt/hostedtoolcache/CodeQL + sudo docker image prune --all --force >/dev/null 2>&1 || true + echo "Disk space after cleanup:" + df -h / + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: From 5842536812b3b27ec4f72d5f97eaba75cd29a2c7 Mon Sep 17 00:00:00 2001 From: Adil Atalay Hamamcioglu <38231748+recoskyler@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:02:30 +0000 Subject: [PATCH 5/6] fix: slim images and free CI disk to resolve "no space left on device" The variant builds (php, bun, rust, flutter) were exhausting GitHub runner disk during image export/load. Two root causes addressed: - headroom-ai[all] pulled torch + ~2.5GB of bundled CUDA libs (the 3.4GB layer), plus onnxruntime/opencv/sentence-transformers via image/evals/ memory extras. Narrowed to [code,mcp,proxy], which keeps tree-sitter code analysis, MCP, and the proxy server (CPU-only onnxruntime/transformers/ magika, ~500-700MB) with no torch/CUDA. Removed from the base image, so every variant shrinks by ~2.5GB+. - Dropped the full Android Studio IDE (~2-3GB) from the flutter image; flutter/Android builds use the SDK (cmdline-tools, platform-tools, build-tools) installed via sdkmanager. Also expanded the variants' "Free disk space" CI step (full ghcup/ hostedtoolcache/swift removal + swapoff) for headroom margin. Docs/scripts: README drops the Android Studio bullet; validate-flutter- runtime.sh Section 4 skips the IDE-launch check (manual script, not CI). --- .github/workflows/build.yml | 17 +++++++++++++-- .github/workflows/check.yml | 17 +++++++++++++-- README.md | 1 - scripts/setup-claude.sh | 7 +++++-- scripts/validate-flutter-runtime.sh | 21 ++++--------------- .../Dockerfile | 17 ++++++--------- 6 files changed, 45 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf495c9..4ad69ce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -164,9 +164,22 @@ jobs: run: | echo "Disk space before cleanup:" df -h / - sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ - /usr/local/share/boost /opt/hostedtoolcache/CodeQL + sudo rm -rf \ + /usr/local/lib/android \ + /usr/share/dotnet \ + /opt/ghc \ + /usr/local/.ghcup \ + /usr/share/swift \ + /usr/local/share/powershell \ + /usr/local/share/chromium \ + /usr/local/lib/node_modules \ + /usr/local/share/boost \ + /usr/lib/jvm \ + /opt/hostedtoolcache + sudo swapoff -a || true + sudo rm -f /mnt/swapfile /swapfile || true sudo docker image prune --all --force >/dev/null 2>&1 || true + sudo apt-get clean echo "Disk space after cleanup:" df -h / diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 78a648d..9e3f4a6 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -171,9 +171,22 @@ jobs: run: | echo "Disk space before cleanup:" df -h / - sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ - /usr/local/share/boost /opt/hostedtoolcache/CodeQL + sudo rm -rf \ + /usr/local/lib/android \ + /usr/share/dotnet \ + /opt/ghc \ + /usr/local/.ghcup \ + /usr/share/swift \ + /usr/local/share/powershell \ + /usr/local/share/chromium \ + /usr/local/lib/node_modules \ + /usr/local/share/boost \ + /usr/lib/jvm \ + /opt/hostedtoolcache + sudo swapoff -a || true + sudo rm -f /mnt/swapfile /swapfile || true sudo docker image prune --all --force >/dev/null 2>&1 || true + sudo apt-get clean echo "Disk space after cleanup:" df -h / diff --git a/README.md b/README.md index 309a0c5..47f03fd 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,6 @@ Extends the VNC image with Flutter, Rust, and Android tooling. - **Rust** toolchain (rustup, rustfmt, clippy, cargo-watch, cargo-edit, cargo-nextest) - **Android SDK**: cmdline-tools, platform-tools, build-tools (28.0.3 + 35.0.0), API 35 + 36 - **Android Emulator** with SwiftShader (AVD: `flutter_pixel7`, Pixel 7, API 35) -- **Android Studio** Panda 1 (at `/opt/android-studio`) - **Chromium** (`CHROME_EXECUTABLE` set for `flutter run -d chrome`) - **OpenJDK 21** (headless) diff --git a/scripts/setup-claude.sh b/scripts/setup-claude.sh index 0ac7a83..6e4401b 100755 --- a/scripts/setup-claude.sh +++ b/scripts/setup-claude.sh @@ -23,5 +23,8 @@ npx -y skills add -y --global --all vercel-labs/agent-browser npx -y @opengsd/gsd-core@latest --claude --global # --- Headroom --- - -uv tool install "headroom-ai[all]" +# Install only the extras we use: code analysis (tree-sitter), MCP, and the +# proxy server. Deliberately omit ml/image/evals/memory/voice/benchmark — those +# pull torch+CUDA (~2.5GB), onnxruntime/opencv, and sentence-transformers, which +# bloated every image and exhausted CI runner disk. +uv tool install "headroom-ai[code,mcp,proxy]" diff --git a/scripts/validate-flutter-runtime.sh b/scripts/validate-flutter-runtime.sh index f81a4e1..c87bf6c 100755 --- a/scripts/validate-flutter-runtime.sh +++ b/scripts/validate-flutter-runtime.sh @@ -266,23 +266,10 @@ echo "--- Section 3 complete ---" echo "" echo "=== Section 4: Android Studio Launch ===" -/opt/android-studio/bin/studio.sh & -STUDIO_PID=$! - -# Give Studio time to start (it is heavy) -sleep 30 - -if kill -0 "$STUDIO_PID" 2>/dev/null; then - pass "Android Studio is running (PID: $STUDIO_PID)" - if [ "$KERNEL_COMPAT" -eq 1 ]; then - echo "ADB devices (Studio should see emulator):" - adb devices - else - skip "Studio-emulator connection -- emulator skipped due to kernel incompatibility" - fi -else - fail "Android Studio failed to start (PID: $STUDIO_PID exited)" -fi +# The full Android Studio IDE is no longer bundled in the image (removed to keep +# the image slim). Builds/tests use the Android SDK directly. Install the IDE at +# runtime if GUI development over VNC is required. +skip "Android Studio launch -- IDE not bundled (use the Android SDK for builds)" echo "--- Section 4 complete ---" diff --git a/trixie-vnc-flutter-rust-nvm-uv-claude/Dockerfile b/trixie-vnc-flutter-rust-nvm-uv-claude/Dockerfile index e01dd89..8320da2 100644 --- a/trixie-vnc-flutter-rust-nvm-uv-claude/Dockerfile +++ b/trixie-vnc-flutter-rust-nvm-uv-claude/Dockerfile @@ -51,13 +51,11 @@ RUN mkdir -p /home/dev/android-sdk/cmdline-tools \ && mv /tmp/cmdline-tools /home/dev/android-sdk/cmdline-tools/latest \ && rm /tmp/cmdline-tools.zip -# ── Android Studio Panda 1 | 2025.3.1.5 ───────────────────────────────────── - -ARG ANDROID_STUDIO_URL=https://dl.google.com/dl/android/studio/ide-zips/2025.3.1.5/android-studio-2025.3.1.5-linux.tar.gz - -RUN curl -fsSL "$ANDROID_STUDIO_URL" -o /tmp/android-studio.tar.gz \ - && tar -xzf /tmp/android-studio.tar.gz -C /opt/ \ - && rm /tmp/android-studio.tar.gz +# Note: the full Android Studio IDE is intentionally NOT installed. Flutter and +# Android builds rely on the SDK (cmdline-tools, platform-tools, build-tools) +# installed via sdkmanager below; the GUI IDE added ~2-3GB to this image without +# being needed for headless `flutter build`/`test`. Install it at runtime if a +# GUI IDE is required over VNC. # ── Ownership and user switch ──────────────────────────────────────────────── @@ -99,9 +97,6 @@ RUN fvm install stable \ # Accept Flutter-specific Android licenses RUN yes | flutter doctor --android-licenses >/dev/null 2>&1 || true -# Configure Android Studio path for flutter doctor detection -RUN flutter config --android-studio-dir=/opt/android-studio 2>/dev/null || true - # ── Rust (via rustup, installed as user) ────────────────────────────────────── RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ && . "$HOME/.cargo/env" \ @@ -113,7 +108,7 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ RUN echo 'source "$HOME/.cargo/env"' >> /home/dev/.bashrc # ── CLAUDE.md tools append ──────────────────────────────────────────────────── -RUN printf '\n### Flutter & Android\n- `flutter`, `dart` via FVM (`fvm`)\n- Android SDK: cmdline-tools, platform-tools, build-tools 28.0.3 + 35.0.0, API 35 + 36\n- Android Emulator with SwiftShader (AVD: flutter_pixel7, Pixel 7, API 35)\n- Android Studio Panda 1 at /opt/android-studio\n- `chromium` (CHROME_EXECUTABLE set for `flutter run -d chrome`)\n\n### Rust\n- `rustc`, `cargo`, `rustup`, `rustfmt`, `clippy`\n- `cargo-watch`, `cargo-edit`, `cargo-nextest`\n\n### ADB Wireless Debugging (Physical Devices)\n\n**Method A: Wireless Pairing (Android 11+, no USB needed)**\n1. On device: Settings > Developer options > Wireless debugging > Enable\n2. Tap "Pair device with pairing code" -- note IP:PORT and 6-digit code\n3. In terminal: `adb pair :` and enter the code\n4. Note IP:PORT on "Wireless debugging" screen header\n5. Connect: `adb connect :`\n6. Verify: `adb devices`\n\n**Method B: TCP/IP (requires USB initially)**\n1. Connect device via USB to host\n2. On host (not container): `adb tcpip 5555`\n3. Disconnect USB\n4. In terminal: `adb connect :5555`\n5. Verify: `adb devices`\n\n**Network note:** Container must reach device IP. Use `docker run --network host` or ensure same network.\n\n### Android Emulator Known Issues\n\n**Kernel compatibility:** The Android emulator (v36.4.9.0) uses QEMU internally and may segfault on Linux kernel >= 6.17 (e.g., Fedora 43 kernel 6.17.7). This is an upstream QEMU/kernel incompatibility, not a container defect.\n\n**Workarounds:**\n- Use a host with kernel < 6.17 (e.g., Ubuntu 24.04 LTS with kernel 6.8)\n- Wait for a newer Android emulator release with QEMU fix\n- Use CI runners (GitHub Actions Ubuntu runners use compatible kernels)\n- For local development on affected kernels, use ADB wireless debugging with a physical device instead\n\n**KVM acceleration (optional):** If the host has KVM available, pass --device /dev/kvm to docker run for 5-10x faster emulator boot. The emulator auto-detects KVM; SwiftShader software rendering is used as fallback.\n' >> $HOME/.claude/CLAUDE.md +RUN printf '\n### Flutter & Android\n- `flutter`, `dart` via FVM (`fvm`)\n- Android SDK: cmdline-tools, platform-tools, build-tools 28.0.3 + 35.0.0, API 35 + 36\n- Android Emulator with SwiftShader (AVD: flutter_pixel7, Pixel 7, API 35)\n- `chromium` (CHROME_EXECUTABLE set for `flutter run -d chrome`)\n\n### Rust\n- `rustc`, `cargo`, `rustup`, `rustfmt`, `clippy`\n- `cargo-watch`, `cargo-edit`, `cargo-nextest`\n\n### ADB Wireless Debugging (Physical Devices)\n\n**Method A: Wireless Pairing (Android 11+, no USB needed)**\n1. On device: Settings > Developer options > Wireless debugging > Enable\n2. Tap "Pair device with pairing code" -- note IP:PORT and 6-digit code\n3. In terminal: `adb pair :` and enter the code\n4. Note IP:PORT on "Wireless debugging" screen header\n5. Connect: `adb connect :`\n6. Verify: `adb devices`\n\n**Method B: TCP/IP (requires USB initially)**\n1. Connect device via USB to host\n2. On host (not container): `adb tcpip 5555`\n3. Disconnect USB\n4. In terminal: `adb connect :5555`\n5. Verify: `adb devices`\n\n**Network note:** Container must reach device IP. Use `docker run --network host` or ensure same network.\n\n### Android Emulator Known Issues\n\n**Kernel compatibility:** The Android emulator (v36.4.9.0) uses QEMU internally and may segfault on Linux kernel >= 6.17 (e.g., Fedora 43 kernel 6.17.7). This is an upstream QEMU/kernel incompatibility, not a container defect.\n\n**Workarounds:**\n- Use a host with kernel < 6.17 (e.g., Ubuntu 24.04 LTS with kernel 6.8)\n- Wait for a newer Android emulator release with QEMU fix\n- Use CI runners (GitHub Actions Ubuntu runners use compatible kernels)\n- For local development on affected kernels, use ADB wireless debugging with a physical device instead\n\n**KVM acceleration (optional):** If the host has KVM available, pass --device /dev/kvm to docker run for 5-10x faster emulator boot. The emulator auto-detects KVM; SwiftShader software rendering is used as fallback.\n' >> $HOME/.claude/CLAUDE.md # ── Build Verification ──────────────────────────────────────────────────────── RUN flutter doctor -v From f24709d05b269aae71be63d790f53c9850d24963 Mon Sep 17 00:00:00 2001 From: Adil Atalay Hamamcioglu <38231748+recoskyler@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:32:13 +0000 Subject: [PATCH 6/6] fix: install kubectl from dl.k8s.io static binary instead of apt repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pkgs.k8s.io apt repo intermittently 403s on CI runners (transient CDN rate-limiting) when fetching Release.key, non-deterministically failing the base image build — it broke the docker base while the no-docker base passed in the same run. Switch to the canonical static binary from dl.k8s.io with --retry-all-errors (retries 4xx, not just 5xx/transport), removing the apt repo, GPG key, and apt-get update round-trips entirely. --- base/Dockerfile | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/base/Dockerfile b/base/Dockerfile index 65013d4..38f56f2 100644 --- a/base/Dockerfile +++ b/base/Dockerfile @@ -133,12 +133,15 @@ RUN curl -fsSL https://apt.releases.hashicorp.com/gpg | gpg --dearmor -o /usr/sh && apt-get update \ && apt-get -y install --no-install-recommends terraform -# kubectl (Kubernetes apt repo v1.32) -RUN curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key | gpg --dearmor -o /usr/share/keyrings/kubernetes-apt-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /" \ - | tee /etc/apt/sources.list.d/kubernetes.list \ - && apt-get update \ - && apt-get -y install --no-install-recommends kubectl +# kubectl (official static binary from dl.k8s.io) +# Avoids the pkgs.k8s.io apt repo, whose Release.key fetch intermittently 403s on +# CI runners (transient CDN rate-limiting). --retry-all-errors retries 4xx too. +RUN ARCH=$(dpkg --print-architecture) \ + && KVER=$(curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 https://dl.k8s.io/release/stable-1.32.txt) \ + && curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 \ + "https://dl.k8s.io/release/${KVER}/bin/linux/${ARCH}/kubectl" -o /usr/local/bin/kubectl \ + && chmod +x /usr/local/bin/kubectl \ + && kubectl version --client # ── User setup ───────────────────────────────────────────────────────────────