diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..c388977b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,17 @@
+.git
+.github
+.husky
+
+node_modules
+ui/node_modules
+
+dist
+ui/dist
+ui/.vite
+
+coverage
+ui/coverage
+
+*.log
+.DS_Store
+
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..8f09b6d6
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,68 @@
+# syntax=docker/dockerfile:1
+
+FROM node:20-bookworm-slim AS build
+
+SHELL ["/bin/bash", "-lc"]
+
+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/*
+
+RUN curl -fsSL https://bun.sh/install | bash
+
+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
+
+
+FROM node:20-bookworm-slim AS runtime
+
+SHELL ["/bin/bash", "-lc"]
+
+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/*
+
+RUN curl -fsSL https://bun.sh/install | bash
+
+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
+
+RUN npm install -g @google/gemini-cli @vibe-kit/grok-cli @anthropic-ai/claude-code \
+ && npm install -g @kaitranntt/ccs --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..1a0f46cd
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,123 @@
+
+
+# CCS Dashboard - Docker
+
+
+
+### 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` and `/home/node/.opencode` via named volumes.
+
+## 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
+```
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 00000000..12f6eee7
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,38 @@
+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
+ 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
+
+volumes:
+ ccs_home:
+ claude_home:
+ opencode_home:
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
new file mode 100644
index 00000000..d1357da6
--- /dev/null
+++ b/docker/entrypoint.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ccs_home_dir="${CCS_HOME_DIR:-/home/node/.ccs}"
+
+mkdir -p "$ccs_home_dir"
+
+if [ "$(id -u)" = "0" ]; then
+ chown -R node:node "$ccs_home_dir" || true
+fi
+
+if [ "$#" -eq 0 ]; then
+ echo "[X] No command provided" >&2
+ exit 1
+fi
+
+if [ "$(id -u)" = "0" ]; then
+ cmd="$(printf '%q ' "$@")"
+ exec su -s /bin/bash node -c "exec ${cmd}"
+fi
+
+exec "$@"
+