diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..0415d714 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,47 @@ +# Git and GitHub +.git +.github +.husky + +# Dependencies +node_modules +ui/node_modules + +# Build outputs (rebuilt in container) +dist +ui/dist +ui/.vite + +# Test and coverage +coverage +ui/coverage +tests +**/__tests__ + +# Environment files (SECURITY: prevent secret leakage) +.env +.env.* +.env.local +.env.production +.env.development + +# Documentation (not needed for runtime) +docs +*.md +!README.md +!LICENSE + +# Logs and OS files +*.log +.DS_Store +Thumbs.db + +# IDE and editor +.vscode +.idea +*.swp +*.swo + +# Misc +*.tgz +*.tar.gz diff --git a/README.md b/README.md index 92adff4a..a0382846 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ ccs config # Opens http://localhost:3000 ``` +Want to run the dashboard in Docker? See `docker/README.md`. + ### 3. Configure Your Accounts The dashboard provides visual management for all account types: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..09b0be39 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,85 @@ +# syntax=docker/dockerfile:1 + +# ============================================================================= +# Build stage: compile TypeScript and build UI +# ============================================================================= +FROM node:20-bookworm-slim AS build + +SHELL ["/bin/bash", "-lc"] + +# Pin bun version for reproducible builds +ARG BUN_VERSION=1.2.21 +ENV BUN_INSTALL=/usr/local/bun +ENV PATH="$BUN_INSTALL/bin:$PATH" + +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl ca-certificates unzip \ + && rm -rf /var/lib/apt/lists/* + +# Install specific bun version +RUN curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" + +WORKDIR /app + +# Dependency install layer (avoid running install scripts inside the image build) +COPY package.json bun.lock bunfig.toml ./ +COPY ui/package.json ui/bun.lock ./ui/ + +RUN bun install --frozen-lockfile --ignore-scripts \ + && (cd ui && bun install --frozen-lockfile --ignore-scripts) + +COPY . . + +RUN bun run build:all + +# Validate build artifacts exist +RUN test -d dist && test -d lib && echo "[OK] Build artifacts validated" + +# ============================================================================= +# Runtime stage: minimal production image +# ============================================================================= +FROM node:20-bookworm-slim AS runtime + +SHELL ["/bin/bash", "-lc"] + +# Pin bun version for reproducible builds +ARG BUN_VERSION=1.2.21 +ENV BUN_INSTALL=/usr/local/bun +ENV PATH="$BUN_INSTALL/bin:/home/node/.opencode/bin:$PATH" + +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl ca-certificates unzip \ + && rm -rf /var/lib/apt/lists/* + +# Install specific bun version +RUN curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" + +WORKDIR /app + +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile --production --ignore-scripts + +COPY docker/entrypoint.sh /usr/local/bin/ccs-entrypoint +RUN chmod +x /usr/local/bin/ccs-entrypoint + +COPY --from=build /app/dist ./dist +COPY --from=build /app/lib ./lib +COPY --from=build /app/config ./config +COPY --from=build /app/scripts ./scripts +COPY --from=build /app/README.md ./README.md +COPY --from=build /app/LICENSE ./LICENSE + +# Install AI CLI tools (using latest - pin versions in production if needed) +# These are optional tools for docker exec usage +RUN npm install -g @google/gemini-cli @vibe-kit/grok-cli @anthropic-ai/claude-code \ + && npm install -g @kaitranntt/ccs --force \ + && npm cache clean --force \ + && su -s /bin/bash node -c 'curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path' \ + && ln -sf /app/dist/ccs.js /usr/local/bin/ccs + +ENV CCS_PORT=3000 +EXPOSE 3000 8317 + +ENTRYPOINT ["/usr/local/bin/ccs-entrypoint"] + +CMD ["bash", "-c", "node dist/ccs.js config --port ${CCS_PORT}"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..d0204282 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,205 @@ +
+ +# CCS Dashboard - Docker + +![CCS Logo](../assets/ccs-logo-medium.png) + +### Run the CCS Config Dashboard in Docker. +Persistent config, restart on reboot. + +**[Back to README](../README.md)** + +
+ +
+ +## Quick Start (Docker Run) + +```bash +docker build -f docker/Dockerfile -t ccs-dashboard:latest . +docker run -d \ + --name ccs-dashboard \ + --restart unless-stopped \ + -p 3000:3000 \ + -p 8317:8317 \ + -e CCS_PORT=3000 \ + -v ccs_home:/home/node/.ccs \ + ccs-dashboard:latest +``` + +Open `http://localhost:3000` (Dashboard). + +CCS also starts CLIProxy on `http://localhost:8317` (used by Dashboard features and OAuth providers). + +## Environment Variables + +Common CCS environment variables (from the docs): + +- Docs: [Environment variables](https://docs.ccs.kaitran.ca/getting-started/configuration#environment-variables) + +- `CCS_CONFIG`: override config file path +- `CCS_UNIFIED_CONFIG=1`: force unified YAML config loader +- `CCS_MIGRATE=1`: trigger config migration +- `CCS_SKIP_MIGRATION=1`: skip migrations +- `CCS_DEBUG=1`: enable verbose logs +- `NO_COLOR=1`: disable ANSI colors +- `CCS_SKIP_PREFLIGHT=1`: skip API key validation checks +- `CCS_WEBSEARCH_SKIP=1`: skip WebSearch hook integration +- Proxy: `CCS_PROXY_HOST`, `CCS_PROXY_PORT`, `CCS_PROXY_PROTOCOL`, `CCS_PROXY_AUTH_TOKEN`, `CCS_PROXY_TIMEOUT`, `CCS_PROXY_FALLBACK_ENABLED`, `CCS_ALLOW_SELF_SIGNED` + +Example (passing env vars to the running container): + +```bash +docker run -d \ + --name ccs-dashboard \ + --restart unless-stopped \ + -p 3000:3000 \ + -p 8317:8317 \ + -e CCS_PORT=3000 \ + -e CCS_DEBUG=1 \ + -e NO_COLOR=1 \ + -e CCS_PROXY_HOST="proxy.example.com" \ + -e CCS_PROXY_PORT=443 \ + -e CCS_PROXY_PROTOCOL="https" \ + -v ccs_home:/home/node/.ccs \ + ccs-dashboard:latest +``` + +## Useful Commands + +```bash +docker logs -f ccs-dashboard +docker stop ccs-dashboard +docker start ccs-dashboard +docker rm -f ccs-dashboard +``` + +## Docker Compose (Optional) + +Using the included `docker/docker-compose.yml`: + +```bash +docker-compose -f docker/docker-compose.yml up --build -d +docker-compose -f docker/docker-compose.yml logs -f +``` + +Stop: + +```bash +docker-compose -f docker/docker-compose.yml down +``` + +## Persistence + +- CCS stores data in `/home/node/.ccs` inside the container. +- The examples use a named volume (`ccs_home`) to persist that data. +- Compose also persists `/home/node/.claude`, `/home/node/.opencode`, and `/home/node/.grok-cli` via named volumes. + +## Resource Limits + +For production deployments, limit container resources: + +```bash +docker run -d \ + --name ccs-dashboard \ + --restart unless-stopped \ + --memory=1g \ + --cpus=2 \ + -p 3000:3000 \ + -p 8317:8317 \ + -v ccs_home:/home/node/.ccs \ + ccs-dashboard:latest +``` + +Docker Compose includes default limits (1GB RAM, 2 CPUs). Adjust in `docker-compose.yml` under `deploy.resources`. + +## Graceful Shutdown + +CCS handles `SIGTERM` gracefully. When stopping the container: + +```bash +docker stop ccs-dashboard # Sends SIGTERM, waits 10s, then SIGKILL +docker stop -t 30 ccs-dashboard # Wait 30s for graceful shutdown +``` + +The `init: true` in docker-compose.yml ensures proper signal forwarding. + +## Troubleshooting + +### Permission Errors (EACCES) + +If you see permission errors on startup: + +```bash +# Check volume permissions +docker exec ccs-dashboard ls -la /home/node/.ccs + +# Fix by recreating volumes +docker-compose down -v +docker-compose up -d +``` + +### Port Already in Use + +```bash +# Check what's using the port +lsof -i :3000 +lsof -i :8317 + +# Use different ports +docker run -p 4000:3000 -p 9317:8317 ... + +# Or with compose +CCS_DASHBOARD_PORT=4000 CCS_CLIPROXY_PORT=9317 docker-compose up -d +``` + +### Container Keeps Restarting + +```bash +# Check logs for errors +docker logs ccs-dashboard --tail 50 + +# Check container health +docker inspect ccs-dashboard --format='{{.State.Health.Status}}' +``` + +### Debug Mode + +Enable verbose logging: + +```bash +docker run -e CCS_DEBUG=1 ... +``` + +## Examples: Claude + Gemini inside Docker + +Open a shell inside the running container: + +```bash +docker exec -it ccs-dashboard bash +``` + +Claude (non-interactive / print mode): + +```bash +docker exec -it ccs-dashboard claude -p "Hello from Docker" +``` + +Gemini (one-shot prompt): + +```bash +docker exec -it ccs-dashboard gemini "Hello from Docker" +``` + +If you need to configure credentials, do it according to each CLI's docs: + +```bash +docker exec -it ccs-dashboard claude --help +docker exec -it ccs-dashboard gemini --help +``` + +## Security Notes + +- **Secrets**: For sensitive values like `CCS_PROXY_AUTH_TOKEN`, consider using Docker secrets or a `.env` file (not committed to git). +- **Network**: The container exposes ports 3000 and 8317. In production, use a reverse proxy (nginx, traefik) with TLS. +- **Updates**: Regularly rebuild the image to get security patches: `docker-compose build --pull` diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..1cd63701 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,53 @@ +# CCS Dashboard Docker Compose +# See docker/README.md for documentation +services: + ccs-dashboard: + image: ccs-dashboard:latest + build: + context: .. + dockerfile: docker/Dockerfile + restart: unless-stopped + init: true + ports: + - "${CCS_DASHBOARD_PORT:-3000}:3000" + - "${CCS_CLIPROXY_PORT:-8317}:8317" + environment: + CCS_PORT: 3000 + CCS_DEBUG: "${CCS_DEBUG:-}" + NO_COLOR: "${NO_COLOR:-}" + CCS_SKIP_PREFLIGHT: "${CCS_SKIP_PREFLIGHT:-}" + CCS_WEBSEARCH_SKIP: "${CCS_WEBSEARCH_SKIP:-}" + CCS_PROXY_HOST: "${CCS_PROXY_HOST:-}" + CCS_PROXY_PORT: "${CCS_PROXY_PORT:-}" + CCS_PROXY_PROTOCOL: "${CCS_PROXY_PROTOCOL:-}" + CCS_PROXY_AUTH_TOKEN: "${CCS_PROXY_AUTH_TOKEN:-}" + CCS_PROXY_TIMEOUT: "${CCS_PROXY_TIMEOUT:-}" + CCS_PROXY_FALLBACK_ENABLED: "${CCS_PROXY_FALLBACK_ENABLED:-}" + CCS_ALLOW_SELF_SIGNED: "${CCS_ALLOW_SELF_SIGNED:-}" + volumes: + - ccs_home:/home/node/.ccs + - claude_home:/home/node/.claude + - opencode_home:/home/node/.opencode + - grok_home:/home/node/.grok-cli + # Healthcheck uses internal ports (3000/8317) which are fixed + healthcheck: + test: ["CMD-SHELL", "curl -fsS --max-time 2 http://localhost:3000/ >/dev/null && curl -sS --max-time 2 http://127.0.0.1:8317/ >/dev/null"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 30s + # Resource limits (adjust based on workload) + deploy: + resources: + limits: + memory: 1G + cpus: '2' + reservations: + memory: 256M + cpus: '0.5' + +volumes: + ccs_home: + claude_home: + opencode_home: + grok_home: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 00000000..1cca373d --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +ccs_home_dir="${CCS_HOME_DIR:-/home/node/.ccs}" + +mkdir -p "$ccs_home_dir" + +# Fix volume permissions if running as root +if [ "$(id -u)" = "0" ]; then + if ! chown -R node:node "$ccs_home_dir" 2>/dev/null; then + echo "[!] Warning: Could not change ownership of $ccs_home_dir (read-only volume?)" >&2 + fi +fi + +# Show usage if no command provided +if [ "$#" -eq 0 ]; then + echo "[X] No command provided" >&2 + echo "" >&2 + echo "Usage: docker run ccs-dashboard " >&2 + echo "" >&2 + echo "Examples:" >&2 + echo " docker run ccs-dashboard node dist/ccs.js config" >&2 + echo " docker run ccs-dashboard ccs --help" >&2 + echo "" >&2 + exit 1 +fi + +# Drop privileges from root to node user +if [ "$(id -u)" = "0" ]; then + cmd="$(printf '%q ' "$@")" + exec su -s /bin/bash node -c "exec ${cmd}" +fi + +exec "$@"