diff --git a/.github/workflows/docs-parity.yml b/.github/workflows/docs-parity.yml
new file mode 100644
index 00000000..f51a316d
--- /dev/null
+++ b/.github/workflows/docs-parity.yml
@@ -0,0 +1,31 @@
+name: Docs – Quickstart Snippet Parity
+
+on:
+ push:
+ paths:
+ - "docs/quickstart-snippet.md"
+ - "README.md"
+ - "docker/README.md"
+ - "tests/docs/quickstart-parity.sh"
+ - ".github/workflows/docs-parity.yml"
+ pull_request:
+ paths:
+ - "docs/quickstart-snippet.md"
+ - "README.md"
+ - "docker/README.md"
+ - "tests/docs/quickstart-parity.sh"
+ - ".github/workflows/docs-parity.yml"
+
+jobs:
+ quickstart-parity:
+ name: Assert quickstart snippet matches in README.md and docker/README.md
+ runs-on: [self-hosted, linux, x64, cliproxy]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+
+ - name: Run quickstart parity check
+ run: bash tests/docs/quickstart-parity.sh
diff --git a/README.md b/README.md
index 2f2aef24..cd4bce29 100644
--- a/README.md
+++ b/README.md
@@ -22,21 +22,23 @@ Anthropic-compatible APIs without config thrash.
> **[Docker]** `ghcr.io/kaitranntt/ccs-dashboard:latest` is deprecated. Use `ghcr.io/kaitranntt/ccs:latest` instead. See [#1251](https://github.com/kaitranntt/ccs/issues/1251) and [docker/README.md](docker/README.md#choosing-an-image) for migration details. To wire a sibling container to CLIProxy, see [Connect your app to CLIProxy](docker/README.md#connect-your-app-to-cliproxy).
-## Why CCS
+
+## Quick Start (Docker)
-CCS gives you one stable command surface while letting you switch between:
+With Docker installed:
-- multiple runtimes such as Claude Code, Factory Droid, and Codex CLI
-- multiple Claude subscriptions and isolated account contexts
-- OAuth providers like Codex, Kiro, Claude, Qwen, Kimi, and more, with legacy
- Copilot compatibility for existing setups
-- API and local-model profiles like GLM, Kimi, OpenRouter, Ollama, llama.cpp,
- Novita, and Alibaba Coding Plan
+```bash
+curl -fsSL https://ccs.kaitran.ca/docker-compose.yaml -o docker-compose.yaml
+docker compose up -d
+```
-The goal is simple: stop rewriting config files, stop breaking active sessions,
-and move between providers in seconds.
+Dashboard at http://localhost:3000 · CLIProxy at http://localhost:8317.
-## Quick Start
+Need a corporate-proxy alternative? Download directly:
+`https://github.com/kaitranntt/ccs/blob/main/docker/compose.yaml`
+
+
+## Install on Host (npm)
```bash
npm install -g @kaitranntt/ccs
@@ -53,6 +55,20 @@ ccs glm
ccs ollama
```
+## Why CCS
+
+CCS gives you one stable command surface while letting you switch between:
+
+- multiple runtimes such as Claude Code, Factory Droid, and Codex CLI
+- multiple Claude subscriptions and isolated account contexts
+- OAuth providers like Codex, Kiro, Claude, Qwen, Kimi, and more, with legacy
+ Copilot compatibility for existing setups
+- API and local-model profiles like GLM, Kimi, OpenRouter, Ollama, llama.cpp,
+ Novita, and Alibaba Coding Plan
+
+The goal is simple: stop rewriting config files, stop breaking active sessions,
+and move between providers in seconds.
+
## OpenAI-Compatible Routing
CCS can now bridge Claude Code into OpenAI-compatible providers through a local
diff --git a/docker/README.md b/docker/README.md
index 5a8132df..97895ab3 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -11,8 +11,29 @@ Persistent config, restart on reboot.
+> **[Deprecation]** `ghcr.io/kaitranntt/ccs-dashboard:latest` is deprecated.
+> Migrate to `ghcr.io/kaitranntt/ccs:latest`. See [Migration](#migration-from-ccs-dashboardlatest) below.
+
+
+## Quick Start (Docker)
+
+With Docker installed:
+
+```bash
+curl -fsSL https://ccs.kaitran.ca/docker-compose.yaml -o docker-compose.yaml
+docker compose up -d
+```
+
+Dashboard at http://localhost:3000 · CLIProxy at http://localhost:8317.
+
+Need a corporate-proxy alternative? Download directly:
+`https://github.com/kaitranntt/ccs/blob/main/docker/compose.yaml`
+
+
+---
+
## Choosing an image
| Tag | Use | Approx. size | Status |
@@ -23,113 +44,11 @@ Persistent config, restart on reboot.
Both `ccs:latest` and `ccs:full` also publish pinned version tags (`ccs:..`, `ccs:.`, `ccs:`) for reproducible deployments. The `:full` variants carry the `full-` prefix: `ccs:full-`, `ccs:full-`, etc.
-## Connect Your App to CLIProxy
-
-The CCS container joins a Docker network named `ccs-net`. This network name is a **stable, public contract** — it will not change without a SemVer-major release.
-
-### Network Contract
-
-| Resource | Stable name | Notes |
-|---|---|---|
-| Network | `ccs-net` | Attach any sibling container to this network |
-| Service DNS | `ccs` | Resolves to the CCS container from inside `ccs-net` |
-| CLIProxy port | `8317` | OAuth proxy — use as `OPENAI_BASE_URL` / `CLIPROXY_URL` |
-| Dashboard port | `3000` | Web UI |
-| Env-friendly URL | `http://ccs:8317` | Drop into your app's env without port-mapping on the host |
-
-### Pattern A — Same Compose File
-
-Declare `ccs-net` as external in your own compose file and add your service to it:
-
-```yaml
-services:
- my-app:
- image: my-app:latest
- environment:
- CLIPROXY_URL: http://ccs:8317
- networks:
- - ccs-net
-
-networks:
- ccs-net:
- external: true
-```
-
-Start CCS first so the network exists:
-
-```bash
-docker compose -f docker/compose.yaml up -d # or: ccs docker up
-docker compose -f my-app/compose.yaml up -d
-```
-
-### Pattern B — `docker run`
-
-Attach a container at runtime without modifying any compose file:
-
-```bash
-docker run --rm \
- --network ccs-net \
- -e CLIPROXY_URL=http://ccs:8317 \
- my-app:latest
-```
-
-### Troubleshooting Network Issues
-
-**Service not resolvable from sibling container**
-
-Verify both containers are on `ccs-net`:
-
-```bash
-docker network inspect ccs-net
-```
-
-The output should list both `ccs` and your app container under `Containers`.
-
-**Network not found**
-
-The `ccs-net` network is created when the CCS stack starts. Run:
-
-```bash
-docker compose -f docker/compose.yaml up -d
-# or: ccs docker up
-```
-
-**Conflict with an existing `ccs-net`**
-
-If you already have a network named `ccs-net` from unrelated tooling, either rename yours or scope
-the CCS project via `COMPOSE_PROJECT_NAME`:
-
-```bash
-COMPOSE_PROJECT_NAME=myproject docker compose -f docker/compose.yaml up -d
-# Network becomes: myproject_ccs-net
-```
-
-Note: scoping changes the network name, so sibling compose files must use the same project name.
-
-**Podman / rootless containers**
-
-On rootless Podman, network names and DNS resolution may behave differently. Verify your Podman
-version supports `--network` with named networks (`podman network ls`) and that `aardvark-dns` or
-equivalent is installed for container-name resolution.
-
-**Low MTU on Hetzner and other cloud providers**
-
-Some cloud environments set a low MTU (e.g., 1450) on their overlay networks. If you see packet
-fragmentation or stalled requests, add a custom MTU to the network in `compose.yaml`:
-
-```yaml
-networks:
- ccs-net:
- name: ccs-net
- driver_opts:
- com.docker.network.driver.mtu: "1450"
-```
-
---
-## Preferred: `ccs docker`
+## Power-user: `ccs docker`
-The CLI now ships a first-class Docker command suite for the integrated CCS + CLIProxy stack:
+The CLI ships a first-class Docker command suite for the integrated CCS + CLIProxy stack:
```bash
ccs docker up
@@ -259,6 +178,8 @@ Expected healthy output:
- CLIProxy health: `cliproxy-port: ok, CLIProxy running`
- Client count matches number of auth token files
+---
+
## Prebuilt Image Quick Start
Pull the recommended minimal image (CCS + CLIProxy, no AI CLIs):
@@ -289,12 +210,7 @@ docker run -d \
Release-tag images are published as `ghcr.io/kaitranntt/ccs:` (minimal) and `ghcr.io/kaitranntt/ccs:full-` (full).
-### Legacy image (deprecated)
-
-The `ghcr.io/kaitranntt/ccs-dashboard:latest` image continues building for 2 more releases but
-emits a deprecation warning on startup. Migrate to `ccs:latest` at your earliest convenience.
-
-## Prebuilt Image Build Locally
+### Build Locally
```bash
docker build -f docker/Dockerfile -t ccs-dashboard:latest .
@@ -312,6 +228,182 @@ Open `http://localhost:3000` (Dashboard).
CCS also starts CLIProxy on `http://localhost:8317` (used by Dashboard features and OAuth providers).
+---
+
+## Connect Your App to CLIProxy
+
+The CCS container joins a Docker network named `ccs-net`. This network name is a **stable, public contract** — it will not change without a SemVer-major release.
+
+### Network Contract
+
+| Resource | Stable name | Notes |
+|---|---|---|
+| Network | `ccs-net` | Attach any sibling container to this network |
+| Service DNS | `ccs` | Resolves to the CCS container from inside `ccs-net` |
+| CLIProxy port | `8317` | OAuth proxy — use as `OPENAI_BASE_URL` / `CLIPROXY_URL` |
+| Dashboard port | `3000` | Web UI |
+| Env-friendly URL | `http://ccs:8317` | Drop into your app's env without port-mapping on the host |
+
+### Pattern A — Same Compose File
+
+Declare `ccs-net` as external in your own compose file and add your service to it:
+
+```yaml
+services:
+ my-app:
+ image: my-app:latest
+ environment:
+ CLIPROXY_URL: http://ccs:8317
+ networks:
+ - ccs-net
+
+networks:
+ ccs-net:
+ external: true
+```
+
+Start CCS first so the network exists:
+
+```bash
+docker compose -f docker/compose.yaml up -d # or: ccs docker up
+docker compose -f my-app/compose.yaml up -d
+```
+
+### Pattern B — `docker run`
+
+Attach a container at runtime without modifying any compose file:
+
+```bash
+docker run --rm \
+ --network ccs-net \
+ -e CLIPROXY_URL=http://ccs:8317 \
+ my-app:latest
+```
+
+### Troubleshooting Network Issues
+
+**Service not resolvable from sibling container**
+
+Verify both containers are on `ccs-net`:
+
+```bash
+docker network inspect ccs-net
+```
+
+The output should list both `ccs` and your app container under `Containers`.
+
+**Network not found**
+
+The `ccs-net` network is created when the CCS stack starts. Run:
+
+```bash
+docker compose -f docker/compose.yaml up -d
+# or: ccs docker up
+```
+
+**Conflict with an existing `ccs-net`**
+
+If you already have a network named `ccs-net` from unrelated tooling, either rename yours or scope
+the CCS project via `COMPOSE_PROJECT_NAME`:
+
+```bash
+COMPOSE_PROJECT_NAME=myproject docker compose -f docker/compose.yaml up -d
+# Network becomes: myproject_ccs-net
+```
+
+Note: scoping changes the network name, so sibling compose files must use the same project name.
+
+**Podman / rootless containers**
+
+On rootless Podman, network names and DNS resolution may behave differently. Verify your Podman
+version supports `--network` with named networks (`podman network ls`) and that `aardvark-dns` or
+equivalent is installed for container-name resolution.
+
+**Low MTU on Hetzner and other cloud providers**
+
+Some cloud environments set a low MTU (e.g., 1450) on their overlay networks. If you see packet
+fragmentation or stalled requests, add a custom MTU to the network in `compose.yaml`:
+
+```yaml
+networks:
+ ccs-net:
+ name: ccs-net
+ driver_opts:
+ com.docker.network.driver.mtu: "1450"
+```
+
+---
+
+## Migration from `ccs-dashboard:latest`
+
+`ghcr.io/kaitranntt/ccs-dashboard:latest` is deprecated and will stop publishing after 2 more
+releases. Migrate to `ghcr.io/kaitranntt/ccs:latest` now.
+
+### Steps
+
+1. **Stop the old stack.**
+
+ ```bash
+ docker compose down
+ # or if running via docker run:
+ docker stop ccs-dashboard && docker rm ccs-dashboard
+ ```
+
+2. **Preserve your data.**
+
+ Existing `~/.ccs` data on the host is not affected by the container change. If you were using
+ a named volume (`ccs_home`), it persists automatically. If you were bind-mounting your host
+ `~/.ccs`, continue doing so — just update the compose file path below.
+
+3. **Get the new compose file.**
+
+ ```bash
+ curl -fsSL https://ccs.kaitran.ca/docker-compose.yaml -o docker-compose.yaml
+ ```
+
+ Or download manually from:
+ `https://github.com/kaitranntt/ccs/blob/main/docker/compose.yaml`
+
+4. **If you were bind-mounting `~/.ccs`** (instead of using a named volume), edit the downloaded
+ `docker-compose.yaml` and replace the `ccs_home` named volume with your bind mount:
+
+ ```yaml
+ volumes:
+ - ~/.ccs:/root/.ccs
+ ```
+
+ Otherwise the default named volume (`ccs_home`) works out of the box. Let compose create it
+ automatically, or create it manually first:
+
+ ```bash
+ docker volume create ccs_home
+ ```
+
+5. **Start the new stack.**
+
+ ```bash
+ docker compose up -d
+ ```
+
+ Dashboard at http://localhost:3000 · CLIProxy at http://localhost:8317.
+
+6. **Verify.**
+
+ ```bash
+ curl -fsS http://localhost:8317/
+ ```
+
+### What changes
+
+| Old | New |
+|---|---|
+| `ghcr.io/kaitranntt/ccs-dashboard:latest` | `ghcr.io/kaitranntt/ccs:latest` |
+| > 600 MB image | < 350 MB image |
+| Monolithic all-in-one | CCS + CLIProxy (AI CLIs optional via `:full`) |
+| No stable network contract | `ccs-net` network, `ccs` service DNS |
+
+---
+
## Environment Variables
Common CCS environment variables (from the docs):
@@ -355,23 +447,6 @@ docker start ccs-dashboard
docker rm -f ccs-dashboard
```
-## Prebuilt Image 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
-```
-
-For the integrated CCS + CLIProxy stack managed by the CLI, use `ccs docker up` instead.
-
## Persistence
- CCS stores data in `/home/node/.ccs` inside the container.
diff --git a/docs/quickstart-snippet.md b/docs/quickstart-snippet.md
new file mode 100644
index 00000000..9400dce0
--- /dev/null
+++ b/docs/quickstart-snippet.md
@@ -0,0 +1,15 @@
+
+## Quick Start (Docker)
+
+With Docker installed:
+
+```bash
+curl -fsSL https://ccs.kaitran.ca/docker-compose.yaml -o docker-compose.yaml
+docker compose up -d
+```
+
+Dashboard at http://localhost:3000 · CLIProxy at http://localhost:8317.
+
+Need a corporate-proxy alternative? Download directly:
+`https://github.com/kaitranntt/ccs/blob/main/docker/compose.yaml`
+
diff --git a/tests/docs/quickstart-parity.sh b/tests/docs/quickstart-parity.sh
new file mode 100755
index 00000000..60cc9622
--- /dev/null
+++ b/tests/docs/quickstart-parity.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+# quickstart-parity.sh
+# Assert that README.md and docker/README.md both contain the canonical quickstart
+# snippet verbatim (anchored by marker comments).
+# Usage: bash tests/docs/quickstart-parity.sh (from repo root)
+set -euo pipefail
+
+SNIPPET=$(awk '//,//' docs/quickstart-snippet.md)
+
+fail=0
+for f in README.md docker/README.md; do
+ file_block=$(awk '//,//' "$f")
+ if ! diff -q <(printf '%s' "$SNIPPET") <(printf '%s' "$file_block") >/dev/null 2>&1; then
+ echo "[X] $f quickstart snippet drift detected" >&2
+ echo "--- canonical (docs/quickstart-snippet.md) ---" >&2
+ printf '%s\n' "$SNIPPET" >&2
+ echo "--- found in $f ---" >&2
+ printf '%s\n' "$file_block" >&2
+ fail=1
+ else
+ echo "[OK] $f snippet matches canonical"
+ fi
+done
+
+exit "$fail"