헤드리스 서버 실행
계속 켜 두는 머신에서 cc_server를 실행한 다음, 데스크톱, 브라우저 또는 휴대폰에서
연결합니다. 이 서버는 Flutter 엔진이 없는 순수 Dart 네이티브 바이너리이므로
macOS, Linux, Windows에서 헤드리스로 실행됩니다. 서버가 데이터베이스를 소유하고,
에이전트를 실행하며, 모든 외부 API 호출을 수행합니다. 클라이언트는 렌더링만
합니다.
Docker로 실행(미리 빌드된 이미지)
섹션 제목: “Docker로 실행(미리 빌드된 이미지)”모든 릴리스는 백엔드, 두 개의 정적 씬 클라이언트, 선택 사항인 페어링 릴레이 등
네 개의 GHCR 이미지로 구성된 자체 호스팅 스택을 게시하며, 퍼블릭 데모 호스트용
cc-server-demo 이미지도 별도로 게시합니다. 자체 호스팅 설치에는 소스 체크아웃이
필요하지 않습니다. 버전 태그(ghcr.io/samuelalev/<image>:v0.0.1-rc.1)를 고정하거나
latest를 사용하세요.
| 이미지 | 무엇인지 | 게시 포트 |
|---|---|---|
ghcr.io/samuelalev/cc-server |
백엔드: 데이터베이스, 에이전트, MCP, 웹훅 | -p 9030:9030 |
ghcr.io/samuelalev/cc-webapp |
웹 클라이언트(정적 nginx) | -p 8080:8080 |
ghcr.io/samuelalev/cc-remote |
휴대폰 컴패니언 PWA(정적 nginx) | -p 8081:8080 |
ghcr.io/samuelalev/cc-signaling-server |
WebRTC 페어링 릴레이(선택 사항) | -p 8788:8788 |
전체 스택을 구성하는 compose 파일:
services: server: image: ghcr.io/samuelalev/cc-server:latest ports: ["9030:9030"] volumes: ["cc_data:/data"] restart: unless-stopped web: image: ghcr.io/samuelalev/cc-webapp:latest ports: ["8080:8080"] restart: unless-stopped remote: image: ghcr.io/samuelalev/cc-remote:latest ports: ["8081:8080"] restart: unless-stopped signaling: image: ghcr.io/samuelalev/cc-signaling-server:latest ports: ["8788:8788"] restart: unless-stopped
volumes: cc_data:이 중 무엇이든 루프백 너머로 노출하기 전에 알아 두면 좋은 점:
-
cc-server이미지는CC_SERVER_INSECURE=1을 함께 제공합니다(게시된 포트 트래픽을 받으려면0.0.0.0에 바인딩해야 하므로). 이는 신뢰할 수 있는 네트워크에서 TLS를 종료하는 리버스 프록시 — Caddy, Traefik, nginx 또는 Cloudflare Tunnel — 뒤에서만 안전합니다. 대신 프로세스 내에서 TLS를 종료하려면 인증서를 마운트하고CC_SERVER_TLS_CERT/CC_SERVER_TLS_KEY를 설정하세요(그런 다음CC_SERVER_INSECURE를 해제). 모든 설정은docker run -e …또는 composeenvironment:블록으로 재정의할 수 있습니다. -
/data가 유일한 상태입니다. SQLite 데이터베이스, 페어링된 기기 시크릿, 다운로드한 모델, 회의 오디오가 모두 볼륨에 저장됩니다. 이 볼륨을 백업하면 컨테이너는 언제든 버려도 됩니다. -
첫 연결 전에 이미지 내부에서 클라이언트를 페어링하세요. 이는 3단계의 컨테이너 버전입니다:
터미널 창 docker run --rm -it -v cc_data:/data \--entrypoint /app/bin/cc_server ghcr.io/samuelalev/cc-server:latest pair -
두 클라이언트 이미지는 정적 nginx 호스트입니다. 런타임에 구성할 것이 없으며, 각 클라이언트의 연결 양식은 서버 URL, 기기 ID, 페어링 키를 입력받습니다. 시그널링 릴레이도 마찬가지로 포트만 필요합니다(
SIGNALING_HOST/SIGNALING_PORT재정의 가능). 릴레이는 상태가 없고 시크릿을 보관하지 않으며 폴백입니다. 휴대폰은 루프백, LAN, 테일넷 또는 직접wss://경로에 연결할 수 있을 때 그것을 사용하고, 어느 것도 닿지 않을 때만 릴레이로 폴백합니다. 클라이언트가 항상 서버에 직접 닿는다면 릴레이는 건너뛰세요.
이 가이드의 나머지 부분에서는 소스에서 서버를 빌드합니다. 여기서 다루는 플래그와 동작은 이미지의 환경 변수가 공급하는 것과 동일합니다.
사전 요구 사항
섹션 제목: “사전 요구 사항”- Dart SDK. 이 저장소는 fvm으로 SDK를 고정하므로 모든 명령 앞에
fvm을 붙입니다. - 이 저장소를 체크아웃한 상태.
- 스테이징 스크립트에 필요한 네이티브 툴체인 — Rust/
cargo, 그리고meson,ninja,pkg-config,git및 C++ 컴파일러. 각 빌드 스크립트는 찾지 못한 항목과 설치 방법을 알려 줍니다. - 접근 가능한 호스트: 로컬 사용이면 루프백, 원격 클라이언트면 TLS가 있는 LAN/공용 주소.
1단계: 네이티브 라이브러리 스테이징
섹션 제목: “1단계: 네이티브 라이브러리 스테이징”네이티브는 필수이며 성능 저하 모드가 없습니다. 다른 무엇보다 먼저 저장소 루트에서 빌드하세요:
scripts/natives/build_natives.sh그러면 rift, fff, tree-sitter와 그 문법, aec, lame,
ccpty, cc_watcher, cc_inference 및 SAML 네이티브가
build/natives/에 빌드됩니다. cc_server 빌드
훅은 거기에 스테이징된 항목을 번들로 다시 내보내므로, 이 단계를 건너뛰면 빌드가
아예 실패하거나 — 아래의 탈출구를 사용하면 — 부팅 사전 검사가 시작을 거부하며
누락된 라이브러리를 알려 주는 바이너리가 만들어집니다.
2단계: 바이너리 빌드
섹션 제목: “2단계: 바이너리 빌드”cd apps/cc_serverfvm dart build cli번들은 바이너리와 함께 libsqlite3와 스테이징된 네이티브를 제공합니다. 시스템
SQLite도, Flutter 엔진도 필요하지 않습니다. 실행 파일은
./build/cli/<os_arch>/bundle/bin/cc_server에 생성됩니다.
3단계: 첫 시작 전에 클라이언트 페어링
섹션 제목: “3단계: 첫 시작 전에 클라이언트 페어링”새 데이터 디렉터리에는 페어링된 기기도, 사전 공유 키도 없으므로 클라이언트의
페어링 키 입력 프롬프트를 아직 충족할 수 없습니다. pair 하위 명령으로 키를
발급하세요. 이 명령은 global.db와 secrets.json을 직접 기록하며, 이미 해당
데이터 디렉터리를 서비스 중인 서버는 재시작 없이 새 기기를 인식합니다:
./build/cli/<os_arch>/bundle/bin/cc_server pair --data-dir ./data --port 9030클라이언트의 연결 양식에 붙여 넣을 세 가지 값을 출력합니다:
Connect a thin client (paste into its connect form): Server ws://localhost:9030/rpc Device id web-client Pairing key <a freshly generated key>유용한 플래그: --device(기본값 web-client, 이 ID로 기기 등급도 결정됩니다 —
desktop, ios, android, 그 외는 웹 클라이언트), --label(기본값은 플랫폼
이름과 이 머신의 호스트 이름), --host(URL에 출력할 호스트, 루프백은 다른
머신에서 접근할 수 없으므로) 및 --client-url. pair를 다시 실행하면 해당
기기의 키가 교체되고 활성 세션이 끊깁니다. 페어링은 워크스페이스를 만들지
않습니다. 연결하는 클라이언트의 온보딩이 첫 워크스페이스의 이름을 정하고
생성합니다.
--client-url을 사용하면 웹 클라이언트를 미리 채운 상태로 여는, 터미널에서
스캔 가능한 QR 코드도 출력합니다:
./build/cli/<os_arch>/bundle/bin/cc_server pair --data-dir ./data --port 9030 \ --bind any --host 192.168.1.42 --client-url https://app.usectrl.devcc_server pair가 발급한 자격 증명에는 만료가 없습니다. 앱에서 발급한 자격
증명은 항상 30일 후 만료됩니다. CLI로 발급한 기기는 사용이 끝나면
설정 → 나 → 내 기기에서 폐기하세요.
4단계: 서버 시작
섹션 제목: “4단계: 서버 시작”루프백에서 — 기본값이며 같은 머신의 클라이언트에 필요한 전부입니다:
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030다른 머신에서 접근하려면 모든 인터페이스에 바인딩합니다. TLS 없는 비루프백
바인딩은 시작 시 거부되므로(Refusing to bind non-loopback address … without TLS) 인증서를 제공하세요:
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030 \ --bind any --tls-cert /path/fullchain.pem --tls-key /path/privkey.pem \ --public-url wss://server.example:9030/rpc또는 신뢰할 수 있는 사설 네트워크의 리버스 프록시에서 TLS를 종료하고 그 뒤에서 평문 바인딩을 선택하세요:
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030 \ --bind any --insecure --public-url wss://server.example/rpc--tls-cert와 --tls-key가 모두 설정되면 --insecure는 무시됩니다. TLS가 항상
우선합니다. 서버는 GET /healthz에서 평문 상태를 insecure: true로 알리고,
클라이언트는 그러한 연결에 안전하지 않음 배지를 표시합니다.
서버는 SIGINT/SIGTERM이 올 때까지 실행되다가 정상적으로 종료됩니다.
5단계: 클라이언트 연결
섹션 제목: “5단계: 클라이언트 연결”3단계의 서버 URL과 페어링 키를 사용해 클라이언트를 지정하세요:
- 데스크톱 앱: 첫 실행 화면(Control Center를 어떻게 실행할까요?)에서 이 앱에서 실행 대신 연결 필드를 채우거나, 나중에 설정 → 서버 → 연결 및 상태에서 서버를 추가합니다. 원격 서버 연결을 참고하세요.
- 웹 클라이언트: app.usectrl.dev(또는 직접 만든 웹 빌드 사본)를 열고 서버 URL, 기기 ID, 페어링 키를 입력합니다.
- 휴대폰: 실행 중인 클라이언트에서 Remote 앱을 페어링합니다 — 기기 페어링을 참고하세요.
6단계: Google Calendar 연결(선택 사항)
섹션 제목: “6단계: Google Calendar 연결(선택 사항)”GUI 없이 워크스페이스의 캘린더를 동기화하려면 기기 코드 흐름을 사용하세요:
GOOGLE_OAUTH_CLIENT_ID=<id> GOOGLE_OAUTH_CLIENT_SECRET=<secret> \ ./build/cli/<os_arch>/bundle/bin/cc_server calendar connect \ --data-dir ./data --workspace <workspace-id>다른 기기에서 승인할 코드와 URL을 출력하고, 리프레시 토큰을 서버 측에 저장한 뒤
종료합니다. 소스 빌드에는 Google 클라이언트가 포함되지 않으므로 id와 secret 환경
변수가 필요합니다 — --google-client-id 플래그는 없습니다.
Google Calendar 연결을 참고하세요.
위에서 사용한 플래그는 첫 설정에 필요한 것들입니다. 전체 범위 — 23개의 플래그 및
환경 변수 쌍, 네 개의 하위 명령, 데이터 디렉터리 레이아웃 — 는
cc_server CLI 레퍼런스에 있습니다.
가장 필요할 가능성이 큰 네 가지:
| 플래그 | 기본값 | 의미 |
|---|---|---|
--data-dir |
OS의 사용자별 애플리케이션 데이터 디렉터리 | 데이터베이스, 시크릿, 모델, 캐시된 미디어 |
--port |
9030 |
TCP 포트(0 = 임시 포트) |
--bind |
loopback |
loopback, 또는 모든 인터페이스면 any(TLS 또는 --insecure 필요) |
--public-url |
바인딩에서 파생됨 | 페어링된 클라이언트에 알리는 RPC URL. 프록시, NAT 또는 터널 뒤에서는 이를 명시적으로 설정하세요 |
기본 데이터 디렉터리는 macOS에서 ~/Library/Application Support/control-center,
Windows에서 %APPDATA%\control-center, 그 외에서는 $XDG_DATA_HOME/control-center
(없으면 ~/.local/share/control-center)입니다. cwd 기준 .cc_server는 홈 또는
앱 데이터 디렉터리를 확인할 수 없을 때만 사용됩니다.
서버가 제공하는 것
섹션 제목: “서버가 제공하는 것”cc_server는 제품 표면의 일부가 아니라 전체입니다: repo-RPC 데이터 경로와 구독,
같은 포트의 POST /mcp 및 GET /sse MCP 도구 엔드포인트, 인바운드 웹훅, 미디어
및 폰트 프록시, 싱글 사인온 콜백, 플릿 리스 프로토콜, 30분 주기의 서버 측 RSS
가져오기, 그리고 벡터 검색(sqlite_vector 확장은 프로세스 전역 자동 확장으로
등록되며, 확장을 사용할 수 없는 곳에서만 전체 텍스트 검색으로 저하됩니다).
디스패처는 상태가 없습니다: 세션별 워크스페이스 바인딩이 없습니다. 모든
워크스페이스 범위 호출은 자체 workspace_id를 가지며, 접근 권한은 페어링 키를
보유했는지가 아니라 호출자의 워크스페이스 멤버십과 역할로 결정됩니다.
포트를 루프백 너머로 노출하기 전에 알아야 할 두 가지:
- MCP는 호스트 외부에서 차단형으로 실패합니다. 토큰이 없는 MCP 표면은 루프백에만 응답합니다.
any로 바인딩해도 이 범위는 넓어지지 않습니다 — 토큰을 구성하기 전까지(mcp.setToken, 또는 설정 → 서버 → MCP 서버) 호스트 외부 요청은 “MCP requires a bearer token to answer non-loopback clients”와 함께 거부됩니다. GET /healthz는 의도적으로 인증이 없습니다. 와일드카드 CORS를 사용하며 서버의 ID, 이름, 신원 지문과 빌드 버전을 게시합니다. 클라이언트는 서버를 고정(pin)하기 위해 지문이 필요하므로 이는 의도된 것이지만, 접근 가능한 모든cc_server가 요청하는 누구에게나 그만큼의 정보를 공개한다는 뜻이기도 합니다.