Files
ccs/docker
Kai (Tam Nhu) TranandGitHub b50c2db3ce docs(docker): P3 — hoist two-command quickstart, restructure docker/README, add parity CI (#1260)
* docs: hoist Docker zero-install quickstart above npm install path

- Create docs/quickstart-snippet.md as canonical source for the
  two-command flow (curl + docker compose up -d), wrapped in
  <!-- quickstart-snippet-start/end --> markers
- Hoist the snippet into README.md immediately below the deprecation
  banner, above all other install paths
- Rename old npm-only "## Quick Start" to "## Install on Host (npm)"
  and move it below the Docker quickstart

* docs(docker): restructure README with zero-install first and migration section

- Reorder top-level sections: zero-install (canonical snippet with
  markers), choosing an image, power-user ccs docker, prebuilt image,
  connect your app to CLIProxy, migration, env vars, troubleshooting
- Add deprecation banner at the top pointing at the migration section
- Add ## Migration from ccs-dashboard:latest section with step-by-step
  instructions covering compose down, data preservation, named volume
  vs bind-mount path, and compose up with the new image
- Keep P1's Choosing an image table and P5's Connect Your App to
  CLIProxy section intact, just repositioned

* test(docs): parity check for quickstart snippet across README files

Assert README.md and docker/README.md both contain the canonical
quickstart block verbatim, anchored by marker comments. Exits non-zero
and prints a diff on any drift.

* ci(docs): wire quickstart-parity test on push and PR

Runs tests/docs/quickstart-parity.sh on self-hosted runner whenever
docs/quickstart-snippet.md, README.md, docker/README.md, or the
test/workflow files themselves change. Fails fast on snippet drift.
2026-05-16 12:56:09 -04:00
..

CCS Docker Deployment

CCS Logo

Run CCS in Docker, locally or over SSH.

Persistent config, restart on reboot.

Back to README

[Deprecation] ghcr.io/kaitranntt/ccs-dashboard:latest is deprecated. Migrate to ghcr.io/kaitranntt/ccs:latest. See Migration below.


Quick Start (Docker)

With Docker installed:

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
ghcr.io/kaitranntt/ccs:latest CCS + CLIProxy, no AI CLIs pre-installed < 350 MB Recommended
ghcr.io/kaitranntt/ccs:full CCS + CLIProxy + claude-code + gemini-cli + grok-cli + opencode < 600 MB Supported
ghcr.io/kaitranntt/ccs-dashboard:latest Legacy all-in-one image > 600 MB Deprecated — migrate to ccs:latest. Sunset after 2 releases. See #1251

Both ccs:latest and ccs:full also publish pinned version tags (ccs:<major>.<minor>.<patch>, ccs:<major>.<minor>, ccs:<major>) for reproducible deployments. The :full variants carry the full- prefix: ccs:full-<ver>, ccs:full-<minor>, etc.


Power-user: ccs docker

The CLI ships a first-class Docker command suite for the integrated CCS + CLIProxy stack:

ccs docker up
ccs docker status
ccs docker logs --follow
ccs docker config
ccs docker update
ccs docker down

Remote deployment stages the bundled Docker assets to ~/.ccs/docker on the target host:

ccs docker up --host my-server
ccs docker --host my-server status
ccs docker status --host my-server
ccs docker logs --host my-server --service ccs --follow
ccs docker config --host my-server

Use a single SSH target or SSH config alias for --host. If you need custom SSH flags such as a port override, configure them in ~/.ssh/config and reference the alias from ccs docker.

The ccs docker flow uses the integrated assets in this directory:

  • docker/Dockerfile.integrated
  • docker/docker-compose.integrated.yml
  • docker/supervisord.conf
  • docker/entrypoint-integrated.sh

Post-Deployment: Enable Dashboard Auth (Required for Remote Access)

When accessing the dashboard from a different machine (not localhost), the API blocks requests with 403 Forbidden unless authentication is configured. Without auth, the dashboard appears empty (no providers, no version).

Set up auth inside the running container:

# Interactive setup (recommended)
docker exec -it ccs-cliproxy ccs config auth setup

# Or via environment variables in docker-compose
environment:
  CCS_DASHBOARD_AUTH_ENABLED: "true"
  CCS_DASHBOARD_USERNAME: "admin"
  CCS_DASHBOARD_PASSWORD_HASH: "<bcrypt-hash>"

Running ccs config auth setup on the outer host shell updates that machine's own ~/.ccs, not the Docker volume mounted into ccs-cliproxy. For the integrated stack, configure auth inside the container or provide the auth env vars in Compose.

Generate a bcrypt hash:

docker exec ccs-cliproxy node -e "console.log(require('bcrypt').hashSync('your-password', 10))"

Note: Do not commit the password hash in docker-compose.yml. Use Docker secrets or a .env file (not tracked in git) for sensitive values like CCS_DASHBOARD_PASSWORD_HASH.

After configuring auth, restart the dashboard:

docker exec ccs-cliproxy supervisorctl -c /etc/supervisord.conf restart ccs-dashboard

If accessing from localhost only (e.g., via SSH tunnel), auth is not required:

ssh -L 3000:localhost:3000 my-server
# Then open http://localhost:3000 in browser

Post-Deployment: Migrate Existing Auth Tokens

If you have existing CLIProxy OAuth tokens from a previous deployment, copy them into the Docker volume:

# Copy auth files into the running container
for f in /path/to/old/auth/*.json; do
  docker cp "$f" ccs-cliproxy:/root/.ccs/cliproxy/auth/
done

# Restart CLIProxy to load new tokens
docker exec ccs-cliproxy supervisorctl -c /etc/supervisord.conf restart cliproxy

For remote deployments via ccs docker up --host:

# Copy tokens into the running container (no root/sudo needed)
scp /path/to/auth/*.json my-server:/tmp/ccs-auth/
ssh my-server 'for f in /tmp/ccs-auth/*.json; do docker cp "$f" ccs-cliproxy:/root/.ccs/cliproxy/auth/; done'

# Restart CLIProxy to load new tokens
ssh my-server "docker exec ccs-cliproxy supervisorctl -c /etc/supervisord.conf restart cliproxy"

# Clean up temp files
ssh my-server "rm -rf /tmp/ccs-auth"

Tip: docker cp is preferred over writing directly to Docker volume mountpoints, which require root access.

Post-Deployment: Verification Checklist

After ccs docker up, verify the deployment:

# 1. Check container is healthy
ccs docker status --host my-server

# 2. Verify CLIProxy responds
curl -fsS http://<host>:8317/

# 3. Check health API (from inside container -- no auth needed)
docker exec ccs-cliproxy curl -fsS http://127.0.0.1:3000/api/health \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'{d[\"summary\"][\"passed\"]} passed, {d[\"summary\"][\"errors\"]} errors')"

# 4. Verify auth tokens loaded (check client count)
docker exec ccs-cliproxy grep "client load complete" /var/log/ccs/cliproxy.log

# 5. Test dashboard API (from remote -- requires auth)
curl -fsS -X POST http://<host>:3000/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"your-password"}'

Expected healthy output:

  • Container status: healthy
  • Both supervisor services: RUNNING
  • 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):

docker run -d \
  --name ccs \
  --restart unless-stopped \
  -p 3000:3000 \
  -p 8317:8317 \
  -e CCS_PORT=3000 \
  -v ccs_home:/root/.ccs \
  ghcr.io/kaitranntt/ccs:latest

Or pull the full image with all 4 AI CLIs pre-installed:

docker run -d \
  --name ccs \
  --restart unless-stopped \
  -p 3000:3000 \
  -p 8317:8317 \
  -e CCS_PORT=3000 \
  -v ccs_home:/root/.ccs \
  ghcr.io/kaitranntt/ccs:full

Release-tag images are published as ghcr.io/kaitranntt/ccs:<version> (minimal) and ghcr.io/kaitranntt/ccs:full-<version> (full).

Build Locally

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).


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:

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:

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:

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:

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:

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:

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:

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.

    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.

    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:

    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:

    docker volume create ccs_home
    
  5. Start the new stack.

    docker compose up -d
    

    Dashboard at http://localhost:3000 · CLIProxy at http://localhost:8317.

  6. Verify.

    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):

  • Docs: 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):

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 \
  ghcr.io/kaitranntt/ccs-dashboard:latest

Useful Commands

docker logs -f ccs-dashboard
docker stop ccs-dashboard
docker start ccs-dashboard
docker rm -f ccs-dashboard

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:

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 \
  ghcr.io/kaitranntt/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:

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:

# 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

# 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

# Check logs for errors
docker logs ccs-dashboard --tail 50

# Check container health
docker inspect ccs-dashboard --format='{{.State.Health.Status}}'

Dashboard Shows Empty (No Providers, Wrong Version)

If the dashboard page loads but shows "0 providers", "Not running", or version "v5.0.0":

Cause: The dashboard API blocks non-localhost requests when auth is disabled (security feature). The page HTML loads from any host, but all API calls return 403.

Fix: Enable dashboard authentication:

docker exec -it ccs-cliproxy ccs config auth setup
docker exec ccs-cliproxy supervisorctl -c /etc/supervisord.conf restart ccs-dashboard

Then log in at the dashboard URL. See Post-Deployment: Enable Dashboard Auth above.

CLIProxy Shows 0 Clients After Token Migration

If CLIProxy logs show "0 clients" after copying auth tokens:

# CLIProxy needs a restart to detect new auth files
docker exec ccs-cliproxy supervisorctl -c /etc/supervisord.conf restart cliproxy

# Verify tokens loaded
docker exec ccs-cliproxy grep "client load complete" /var/log/ccs/cliproxy.log

ETXTBSY Error on First Boot

On first container start, you may see ETXTBSY: text file is busy in dashboard logs. This is a known race condition where the dashboard tries to update the CLIProxy binary while it's already running. The dashboard recovers automatically on the next attempt. No action needed.

Debug Mode

Enable verbose logging:

docker run -e CCS_DEBUG=1 ...

Examples: Claude + Gemini inside Docker

Open a shell inside the running container:

docker exec -it ccs-dashboard bash

Claude (non-interactive / print mode):

docker exec -it ccs-dashboard claude -p "Hello from Docker"

Gemini (one-shot prompt):

docker exec -it ccs-dashboard gemini "Hello from Docker"

If you need to configure credentials, do it according to each CLI's docs:

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