From 309e451e439370e9742e64de9f57385d908b1286 Mon Sep 17 00:00:00 2001 From: gabiudrescu Date: Mon, 2 Mar 2026 17:47:06 +0200 Subject: [PATCH 1/3] feat: add Docker Compose setup with Caddy reverse proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-container architecture behind Caddy (port 8080): - Caddy reverse proxy: /api/* → FastAPI, /* → SvelteKit - API: python:3.11-slim with healthcheck - Frontend: multi-stage node:22-slim build Includes dev override (HMR), read-write override, Makefile for orchestration, GitHub Actions workflow for GHCR image publishing, and dual browser/SSR API URL resolution in config.ts. Co-Authored-By: Claude Opus 4.6 --- .dockerignore | 9 ++++ .github/workflows/docker-publish.yml | 63 ++++++++++++++++++++++++++++ Caddyfile | 9 ++++ Makefile | 50 ++++++++++++++++++++++ api/.dockerignore | 18 ++++++++ api/Dockerfile | 20 +++++++++ docker-compose.dev.yml | 16 +++++++ docker-compose.rw.yml | 5 +++ docker-compose.yml | 38 +++++++++++++++++ frontend/.dockerignore | 14 +++++++ frontend/Dockerfile | 25 +++++++++++ frontend/Dockerfile.dev | 3 ++ frontend/src/lib/config.ts | 26 +++++++++--- 13 files changed, 290 insertions(+), 6 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-publish.yml create mode 100644 Caddyfile create mode 100644 Makefile create mode 100644 api/.dockerignore create mode 100644 api/Dockerfile create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.rw.yml create mode 100644 docker-compose.yml create mode 100644 frontend/.dockerignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/Dockerfile.dev diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..0f15b48d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.github +.idea +.claude +.DS_Store +*.md +captain-hook +hooks +docs diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000..808a34df --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,63 @@ +name: Build and Push Docker Images + +on: + push: + branches: [main] + paths: + - "api/**" + - "frontend/**" + - "Dockerfile*" + - "docker-compose*" + - "Caddyfile" + tags: + - "v*" + +env: + REGISTRY: ghcr.io + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set image metadata + id: meta + run: | + OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + echo "owner=${OWNER}" >> "$GITHUB_OUTPUT" + echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + + - name: Build and push API image + uses: docker/build-push-action@v6 + with: + context: ./api + push: true + tags: | + ${{ env.REGISTRY }}/${{ steps.meta.outputs.owner }}/claude-code-karma-api:latest + ${{ env.REGISTRY }}/${{ steps.meta.outputs.owner }}/claude-code-karma-api:${{ steps.meta.outputs.sha_short }} + cache-from: type=gha,scope=api + cache-to: type=gha,mode=max,scope=api + + - name: Build and push Frontend image + uses: docker/build-push-action@v6 + with: + context: ./frontend + push: true + tags: | + ${{ env.REGISTRY }}/${{ steps.meta.outputs.owner }}/claude-code-karma-frontend:latest + ${{ env.REGISTRY }}/${{ steps.meta.outputs.owner }}/claude-code-karma-frontend:${{ steps.meta.outputs.sha_short }} + cache-from: type=gha,scope=frontend + cache-to: type=gha,mode=max,scope=frontend diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 00000000..95bf7c4c --- /dev/null +++ b/Caddyfile @@ -0,0 +1,9 @@ +:8080 { + handle_path /api/* { + reverse_proxy api:8000 + } + + handle { + reverse_proxy frontend:3000 + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..016525e1 --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +.PHONY: up dev rw down build logs health clean ps + +# Production (default) — read-only ~/.claude mount +up: + docker compose up --build -d + +# Dev frontend with HMR +dev: + docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build + +# Read-write ~/.claude mount +rw: + docker compose -f docker-compose.yml -f docker-compose.rw.yml up --build -d + +# Pull pre-built images from GHCR (no local build) +pull: + docker compose pull + docker compose up -d + +# Stop all containers +down: + docker compose down + +# Build without starting +build: + docker compose build + +# Follow logs +logs: + docker compose logs -f + +# Follow logs for a specific service (usage: make log-api, make log-frontend, make log-caddy) +log-%: + docker compose logs -f $* + +# Health check +health: + @curl -sf http://localhost:8080/api/health | python3 -m json.tool || echo "API not healthy" + +# Show running containers +ps: + docker compose ps + +# Remove containers, volumes, and orphans +clean: + docker compose down -v --remove-orphans + +# Open dashboard in browser +open: + open http://localhost:8080 diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 00000000..3f74764c --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,18 @@ +.git +.gitignore +.claude +.playwright-mcp +__pycache__ +*.pyc +*.pyo +tests/ +benchmarks/ +docs/ +scripts/ +*.md +.env +.env.* +.ruff_cache +.pytest_cache +.coverage +htmlcov/ diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 00000000..90256e07 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN mkdir -p /root/.claude /root/.claude_karma + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 00000000..2ed1264c --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,16 @@ +services: + frontend: + build: + context: ./frontend + dockerfile: Dockerfile.dev + image: "" + volumes: + - ./frontend:/app + - frontend-node-modules:/app/node_modules + command: sh -c "npm install && npm run dev -- --host 0.0.0.0 --port 3000" + environment: + - INTERNAL_API_URL=http://api:8000 + - ORIGIN=http://localhost:8080 + +volumes: + frontend-node-modules: diff --git a/docker-compose.rw.yml b/docker-compose.rw.yml new file mode 100644 index 00000000..19069ba2 --- /dev/null +++ b/docker-compose.rw.yml @@ -0,0 +1,5 @@ +services: + api: + volumes: + - ~/.claude:/root/.claude:rw + - karma-data:/root/.claude_karma diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..4827106d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +services: + caddy: + image: caddy:2-alpine + ports: + - "8080:8080" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + depends_on: + api: + condition: service_healthy + frontend: + condition: service_started + restart: unless-stopped + + api: + image: ghcr.io/jayantdevkar/claude-code-karma-api:latest + build: ./api + volumes: + - ~/.claude:/root/.claude:ro + - karma-data:/root/.claude_karma + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 5s + start_period: 10s + retries: 3 + restart: unless-stopped + + frontend: + image: ghcr.io/jayantdevkar/claude-code-karma-frontend:latest + build: ./frontend + environment: + - INTERNAL_API_URL=http://api:8000 + - ORIGIN=http://localhost:8080 + restart: unless-stopped + +volumes: + karma-data: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..87131348 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,14 @@ +.git +.gitignore +.claude +.playwright-mcp +node_modules +build +.svelte-kit +.vite +docs/ +*.md +.env +.env.* +coverage/ +*.tsbuildinfo diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..f0bfd2e2 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,25 @@ +FROM node:22-slim AS build + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +FROM node:22-slim + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev + +COPY --from=build /app/build ./build + +ENV PORT=3000 +ENV ORIGIN=http://localhost:8080 + +EXPOSE 3000 + +CMD ["node", "build"] diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev new file mode 100644 index 00000000..561e551e --- /dev/null +++ b/frontend/Dockerfile.dev @@ -0,0 +1,3 @@ +FROM node:22-slim + +WORKDIR /app diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts index 382fb55c..7fc66397 100644 --- a/frontend/src/lib/config.ts +++ b/frontend/src/lib/config.ts @@ -2,23 +2,37 @@ * Centralized configuration for Claude Code Karma frontend. * * API Base URL: - * - Uses PUBLIC_API_URL environment variable if set - * - Falls back to http://localhost:8000 for local development + * - Browser: Uses PUBLIC_API_URL env var, or `/api` (for Caddy reverse proxy) + * - SSR: Uses INTERNAL_API_URL env var (e.g. `http://api:8000` in Docker), + * or falls back to `http://localhost:8000` for local development * - * To configure in production: - * - Set PUBLIC_API_URL in your .env file - * - Or set it in your deployment environment + * Local dev: Set PUBLIC_API_URL=http://localhost:8000 in .env + * Docker: INTERNAL_API_URL is set via docker-compose environment */ /** * API base URL for all backend requests. + * + * Two contexts need different URLs: + * - Browser: relative `/api` (routed by Caddy) or PUBLIC_API_URL for local dev + * - SSR (Node server): Docker internal `http://api:8000` via INTERNAL_API_URL + * * @example * ```ts * import { API_BASE } from '$lib/config'; * const response = await fetch(`${API_BASE}/projects`); * ``` */ -export const API_BASE = import.meta.env.PUBLIC_API_URL || 'http://localhost:8000'; +const BROWSER_API = import.meta.env.PUBLIC_API_URL || '/api'; +const SERVER_API = (() => { + try { + return process.env.INTERNAL_API_URL || 'http://localhost:8000'; + } catch { + return 'http://localhost:8000'; + } +})(); + +export const API_BASE = typeof window !== 'undefined' ? BROWSER_API : SERVER_API; /** * API request timeout in milliseconds (default: 30 seconds) From 2515adc2b8d3f48092b1f6b9a5f85a07469390d5 Mon Sep 17 00:00:00 2001 From: gabiudrescu Date: Mon, 2 Mar 2026 18:23:17 +0200 Subject: [PATCH 2/3] fix: use absolute URL for browser API_BASE to fix new URL() calls Components like AgentList, SkillViewer, ToolList use new URL(API_BASE + path) which requires an absolute URL. The relative /api path caused "Failed to construct URL: Invalid URL" errors in the browser. Co-Authored-By: Claude Opus 4.6 --- frontend/src/lib/config.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts index 7fc66397..f3655a3c 100644 --- a/frontend/src/lib/config.ts +++ b/frontend/src/lib/config.ts @@ -23,7 +23,6 @@ * const response = await fetch(`${API_BASE}/projects`); * ``` */ -const BROWSER_API = import.meta.env.PUBLIC_API_URL || '/api'; const SERVER_API = (() => { try { return process.env.INTERNAL_API_URL || 'http://localhost:8000'; @@ -32,7 +31,10 @@ const SERVER_API = (() => { } })(); -export const API_BASE = typeof window !== 'undefined' ? BROWSER_API : SERVER_API; +export const API_BASE = + typeof window !== 'undefined' + ? (import.meta.env.PUBLIC_API_URL || `${window.location.origin}/api`) + : SERVER_API; /** * API request timeout in milliseconds (default: 30 seconds) From 0c116be41e10ca96a8dc210f782baa1a13b37eb3 Mon Sep 17 00:00:00 2001 From: gabiudrescu Date: Mon, 2 Mar 2026 18:44:17 +0200 Subject: [PATCH 3/3] docs: add Docker quick start to CLAUDE.md and README.md Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 20 ++++++++++++++++++++ README.md | 28 +++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index ef5925e7..5f8be1ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,6 +25,26 @@ npm install && npm run dev Open http://localhost:5173 to view the dashboard. +### Docker Quick Start + +```bash +# Production (read-only ~/.claude mount) +make up + +# Dev frontend with HMR +make dev + +# Health check +make health + +# Stop +make down +``` + +Open http://localhost:8080 to view the dashboard. + +See `Makefile` for all available targets. + ## Repository Structure This is a monorepo with all components in a single repository: diff --git a/README.md b/README.md index 8e59d184..f773586c 100644 --- a/README.md +++ b/README.md @@ -199,8 +199,34 @@ Track which skills are invoked across sessions, grouped by plugin or shown indiv ## Quick Start +### Docker (recommended) + +```bash +git clone https://github.com/JayantDevkar/claude-code-karma.git +cd claude-code-karma + +# Start all services (API + Frontend + Caddy reverse proxy) +make up + +# Open the dashboard +open http://localhost:8080 +``` + +Other make targets: + +| Command | Description | +|---------|-------------| +| `make up` | Production mode (read-only `~/.claude` mount) | +| `make dev` | Dev mode with frontend HMR | +| `make rw` | Read-write `~/.claude` mount | +| `make health` | Check API health | +| `make logs` | Follow container logs | +| `make down` | Stop all containers | +| `make clean` | Stop and remove volumes | + +### Local (without Docker) + ```bash -# Clone the repository git clone https://github.com/JayantDevkar/claude-code-karma.git cd claude-code-karma