diff --git a/.github/workflows/smoke-test-compose-url.yml b/.github/workflows/smoke-test-compose-url.yml index b9b47577..058167e0 100644 --- a/.github/workflows/smoke-test-compose-url.yml +++ b/.github/workflows/smoke-test-compose-url.yml @@ -59,3 +59,18 @@ jobs: echo "[OK] Both ports healthy" docker compose -f /tmp/ccs-compose.yaml down -v + + - name: Verify ccs-net network contract (sibling DNS resolution) + if: ${{ github.event_name == 'workflow_dispatch' && inputs.do_up == true }} + run: | + set -euo pipefail + + # Checkout repo so tests/docker/network-contract.sh is available + # The script needs to run from a directory that contains docker/compose.yaml. + # We re-use the compose file from the repo rather than /tmp to avoid path drift. + REPO_ROOT="$(mktemp -d)" + git clone --depth=1 --no-tags \ + "https://github.com/kaitranntt/ccs.git" "$REPO_ROOT" + + cd "$REPO_ROOT" + bash tests/docker/network-contract.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f79b9d5..96c5f617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +### Added + +* **docker:** Stable Docker network contract — external network `ccs-net` with service DNS `ccs` resolving to the CCS container. CLIProxy reachable at `http://ccs:8317`; dashboard at `http://ccs:3000`. Sibling containers can attach via `--network ccs-net` or by declaring `ccs-net` as an external network in their own compose file. Changing the network name or service name is a **SemVer-major breaking change**. See [docker/README.md](docker/README.md#connect-your-app-to-cliproxy) for usage patterns and troubleshooting. Verified by `tests/docker/network-contract.sh`. + ### Deprecated * **docker:** `ghcr.io/kaitranntt/ccs-dashboard:latest` Docker image is deprecated — migrate to `ghcr.io/kaitranntt/ccs:latest` (minimal, CCS + CLIProxy) or `ghcr.io/kaitranntt/ccs:full` (with claude-code, gemini-cli, grok-cli, opencode). The legacy image continues publishing for 2 more releases and emits a startup warning. See [#1251](https://github.com/kaitranntt/ccs/issues/1251). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b72b2b00..df420de4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -217,6 +217,18 @@ If you cannot run the full suite, that is still fine for early or docs-only PRs. - Update `ui/src/` and any affected tests. - Run UI validation from `ui/`. +### If you change the Docker network or service name + +The `ccs-net` network name and the `ccs` service name in `docker/compose.yaml` are a **public contract**. Users attach their own containers to `ccs-net` and resolve CLIProxy at `http://ccs:8317`. + +Changing either of these values is a **SemVer-major breaking change**. Before modifying `services.ccs.name` or `networks.ccs-net.name` in `docker/compose.yaml`: + +- Open an issue to discuss the migration path. +- Update `docker/README.md`, `CHANGELOG.md`, and any docs that reference the stable names. +- Bump the major version (via a `feat!:` or `fix!:` commit with a `BREAKING CHANGE:` footer). + +If you are unsure whether your change affects the contract, check `tests/docker/network-contract.sh` — the test will fail if the network or service DNS resolution breaks. + ### If you change config, providers, or architecture - Update the relevant docs in `docs/`. diff --git a/README.md b/README.md index 3dcdc56c..2f2aef24 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ 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. +> **[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 diff --git a/docker/README.md b/docker/README.md index 8297374b..5a8132df 100644 --- a/docker/README.md +++ b/docker/README.md @@ -23,6 +23,110 @@ 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` The CLI now ships a first-class Docker command suite for the integrated CCS + CLIProxy stack: diff --git a/src/cliproxy/quota/quota-manager.ts b/src/cliproxy/quota/quota-manager.ts index 4493aeb6..39841eb6 100644 --- a/src/cliproxy/quota/quota-manager.ts +++ b/src/cliproxy/quota/quota-manager.ts @@ -524,9 +524,8 @@ export async function preflightCheck(provider: CLIProxyProvider): Promise * Fix image analysis configuration issues */ export async function fixImageAnalysisConfig(): Promise { - const { updateConfig, loadOrCreateUnifiedConfig } = await import( - '../../config/config-loader-facade' - ); + const { updateConfig, loadOrCreateUnifiedConfig } = + await import('../../config/config-loader-facade'); const config = loadOrCreateUnifiedConfig(); let fixed = false; diff --git a/tests/docker/network-contract.sh b/tests/docker/network-contract.sh new file mode 100755 index 00000000..2b84cfcc --- /dev/null +++ b/tests/docker/network-contract.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# tests/docker/network-contract.sh +# +# Verifies the stable ccs-net Docker network contract: +# - Network name: ccs-net +# - Service DNS: ccs +# - CLIProxy: http://ccs:8317 +# - Dashboard: http://ccs:3000 +# +# Requires: Docker with compose plugin, internet access to pull curlimages/curl +# Usage: bash tests/docker/network-contract.sh +# (called from repo root so docker/compose.yaml path resolves) +# +set -euo pipefail + +COMPOSE_FILE="docker/compose.yaml" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +log() { printf '[i] %s\n' "$*"; } +ok() { printf '[OK] %s\n' "$*"; } +err() { printf '[X] %s\n' "$*" >&2; } + +# --------------------------------------------------------------------------- +# Bring stack up; register teardown on any exit +# --------------------------------------------------------------------------- +log "Bringing CCS stack up: $COMPOSE_FILE" +docker compose -f "$COMPOSE_FILE" up -d + +cleanup() { + log "Tearing down stack..." + docker compose -f "$COMPOSE_FILE" down -v 2>/dev/null || true +} +trap cleanup EXIT + +# --------------------------------------------------------------------------- +# Wait for healthcheck (max 90s) +# --------------------------------------------------------------------------- +log "Waiting for healthcheck to pass (max 90s)..." +WAIT_MAX=45 # 45 x 2s = 90s +HEALTHY=0 +for _i in $(seq 1 "$WAIT_MAX"); do + STATUS=$( + docker compose -f "$COMPOSE_FILE" ps --format json 2>/dev/null \ + | python3 -c " +import sys, json +data = sys.stdin.read().strip() +if not data: + print('unknown') + raise SystemExit(0) +rows = json.loads('[' + ','.join(data.splitlines()) + ']') +for r in rows: + if 'ccs' in r.get('Service', ''): + print(r.get('Health', 'unknown')) + raise SystemExit(0) +print('unknown') +" 2>/dev/null || echo "unknown" + ) + if [ "$STATUS" = "healthy" ]; then + HEALTHY=1 + break + fi + log "Health: $STATUS (attempt ${_i}/${WAIT_MAX})" + sleep 2 +done + +if [ "$HEALTHY" -ne 1 ]; then + err "Container did not become healthy within 90s" + exit 1 +fi +ok "Container is healthy" + +# --------------------------------------------------------------------------- +# Verify ccs-net network exists on the host +# --------------------------------------------------------------------------- +log "Inspecting ccs-net network..." +docker network inspect ccs-net >/dev/null +ok "ccs-net network exists" + +# --------------------------------------------------------------------------- +# Verify DNS resolution from a sibling container on ccs-net +# --------------------------------------------------------------------------- +log "Testing http://ccs:8317 from sibling container..." +docker run --rm \ + --network ccs-net \ + curlimages/curl:latest \ + -fsS --max-time 10 \ + http://ccs:8317/ \ + >/dev/null +ok "CLIProxy reachable at http://ccs:8317" + +log "Testing http://ccs:3000 from sibling container..." +docker run --rm \ + --network ccs-net \ + curlimages/curl:latest \ + -fsS --max-time 10 \ + http://ccs:3000/ \ + >/dev/null +ok "Dashboard reachable at http://ccs:3000" + +# --------------------------------------------------------------------------- +# Done +# --------------------------------------------------------------------------- +ok "network contract verified"