Einen Headless-Server betreiben
Lass cc_server auf einer Maschine laufen, die an bleibt, und verbinde dich
dann vom Desktop, aus einem Browser oder vom Telefon aus. Der Server ist ein
natives Binary in reinem Dart, ohne Flutter-Engine, und läuft deshalb headless
unter macOS, Linux oder Windows. Ihm gehört die Datenbank, er führt die Agenten
aus und tätigt jeden externen API-Aufruf. Die Clients zeichnen nur.
In Docker betreiben (vorgefertigte Images)
Abschnitt betitelt „In Docker betreiben (vorgefertigte Images)“Jedes Release veröffentlicht einen selbst gehosteten Stack aus vier
GHCR-Images — das Backend, die beiden statischen Thin Clients und das optionale
Kopplungs-Relay — plus ein separates Image cc-server-demo für den öffentlichen
Demo-Host. Eine selbst gehostete Installation braucht kein ausgechecktes
Quell-Repository. Pinne einen Versions-Tag
(ghcr.io/samuelalev/<image>:v0.0.1-rc.1) oder nimm latest.
| Image | Was es ist | Veröffentlicht |
|---|---|---|
ghcr.io/samuelalev/cc-server |
Das Backend: Datenbank, Agenten, MCP, Webhooks | -p 9030:9030 |
ghcr.io/samuelalev/cc-webapp |
Der Web-Client (statisches nginx) | -p 8080:8080 |
ghcr.io/samuelalev/cc-remote |
Die Telefon-Begleit-PWA (statisches nginx) | -p 8081:8080 |
ghcr.io/samuelalev/cc-signaling-server |
Das WebRTC-Kopplungs-Relay (optional) | -p 8788:8788 |
Der ganze Stack als Compose-Datei:
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:Was du wissen solltest, bevor du davon etwas über Loopback hinaus erreichbar machst:
-
Das Image
cc-serverbringtCC_SERVER_INSECURE=1mit (es muss0.0.0.0binden, damit Verkehr über veröffentlichte Ports ankommt). Das ist nur hinter einem TLS-terminierenden Reverse-Proxy sicher — Caddy, Traefik, nginx oder ein Cloudflare Tunnel — in einem vertrauenswürdigen Netz. Um TLS stattdessen im Prozess zu terminieren, häng ein Zertifikat ein und setzeCC_SERVER_TLS_CERT/CC_SERVER_TLS_KEY(und nimm danachCC_SERVER_INSECUREweg). Alles lässt sich mitdocker run -e …oder dem Compose-Blockenvironment:überschreiben. -
Nur
/datahält Zustand. Die SQLite-Datenbanken, die Geheimnisse gekoppelter Geräte, heruntergeladene Modelle und das Audio der Besprechungen liegen alle im Volume. Sichere es, dann ist der Container austauschbar. -
Kopple einen Client aus dem Image heraus, bevor die erste Verbindung steht. Das ist das Container-Gegenstück zu Schritt 3:
Terminalfenster docker run --rm -it -v cc_data:/data \--entrypoint /app/bin/cc_server ghcr.io/samuelalev/cc-server:latest pair -
Die beiden Client-Images sind statische nginx-Hosts. Zur Laufzeit ist nichts zu konfigurieren. Das Verbindungsformular jedes Clients nimmt deine Server-URL, die Geräte-ID und den Kopplungsschlüssel. Das Signalisierungs-Relay braucht ebenfalls nur seinen Port (
SIGNALING_HOST/SIGNALING_PORTlassen sich überschreiben). Es ist zustandslos, hält keine Geheimnisse und ist ein Rückfall: Ein Telefon nimmt Loopback, LAN, Tailnet oder einen direktenwss://-Weg, sobald einer erreichbar ist, und fällt nur auf das Relay zurück, wenn keiner es ist. Lass es weg, wenn deine Clients den Server immer direkt erreichen.
Der Rest dieser Anleitung baut den Server aus dem Quellcode. Die Flags und das Verhalten, die sie behandelt, sind dieselben, die die Umgebungsvariablen des Images setzen.
Voraussetzungen
Abschnitt betitelt „Voraussetzungen“- Das Dart SDK. Dieses Repo pinnt sein SDK mit fvm, deshalb stellst du jedem Befehl
fvmvoran. - Dieses Repository, ausgecheckt.
- Die native Toolchain, die das Bereitstellungs-Skript braucht — Rust/
cargo, dazumeson,ninja,pkg-config,gitund ein C++-Compiler. Jedes Build-Skript nennt, was es nicht findet, und wie du es installierst. - Ein erreichbarer Host: Loopback für den lokalen Gebrauch, oder eine LAN- oder öffentliche Adresse mit TLS für entfernte Clients.
Schritt 1: Die nativen Bibliotheken bereitstellen
Abschnitt betitelt „Schritt 1: Die nativen Bibliotheken bereitstellen“Die nativen Bibliotheken sind erforderlich. Einen eingeschränkten Modus gibt es nicht. Baue sie vom Wurzelverzeichnis des Repos aus, bevor du irgendetwas anderes tust:
scripts/natives/build_natives.shDas baut rift, fff, tree-sitter samt Grammatiken, aec, lame,
ccpty, cc_watcher, cc_inference und das SAML-Native nach
build/natives/. Der Build-Hook von cc_server schreibt, was dort
bereitliegt, erneut ins Bundle. Überspringst du diesen Schritt, schlägt der
Build entweder sofort fehl, oder es entsteht — mit dem Ausweg unten — eine
Binärdatei, deren Startprüfung den Start verweigert und die fehlende
Bibliothek nennt.
Schritt 2: Die Binärdatei bauen
Abschnitt betitelt „Schritt 2: Die Binärdatei bauen“cd apps/cc_serverfvm dart build cliDas Bundle legt libsqlite3 und die bereitgestellten nativen Bibliotheken
neben die Binärdatei. System-SQLite und eine Flutter-Engine brauchst du nicht.
Die ausführbare Datei liegt unter
./build/cli/<os_arch>/bundle/bin/cc_server.
Schritt 3: Einen Client vor dem ersten Start koppeln
Abschnitt betitelt „Schritt 3: Einen Client vor dem ersten Start koppeln“Ein frisches Datenverzeichnis hat kein gekoppeltes Gerät und keinen vorab
geteilten Schlüssel, deshalb lässt sich die Abfrage des Kopplungsschlüssels im
Client noch nicht erfüllen. Stell einen mit dem Unterbefehl pair aus. Er
schreibt global.db und secrets.json direkt. Ein Server, der dieses
Datenverzeichnis schon bedient, nimmt das neue Gerät ohne Neustart auf:
./build/cli/<os_arch>/bundle/bin/cc_server pair --data-dir ./data --port 9030Er gibt drei Werte aus, die du in das Verbindungsformular eines Clients einfügst:
Connect a thin client (paste into its connect form): Server ws://localhost:9030/rpc Device id web-client Pairing key <a freshly generated key>Nützliche Flags: --device (Vorgabe web-client; die ID wählt auch die
Geräteklasse — desktop, ios, android, alles andere ist ein Web-Client),
--label (Vorgabe ist der Plattformname plus der Hostname dieser Maschine),
--host (der Host, der in der URL steht, weil Loopback von einer anderen
Maschine aus nicht erreichbar ist) und --client-url. Ein erneutes pair
rotiert den Schlüssel dieses Geräts und beendet seine laufenden Sitzungen. Die
Kopplung legt nie einen Arbeitsbereich an. Das Onboarding des Clients, der sich
verbindet, benennt den ersten und legt ihn an.
Mit --client-url gibt er zusätzlich einen QR aus, den du im Terminal scannen
kannst. Er öffnet den Web-Client schon ausgefüllt:
./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.devEine Zugangsberechtigung von cc_server pair hat kein Ablaufdatum. Eine
in der App ausgestellte läuft immer nach 30 Tagen ab. Widerrufe ein über die
CLI ausgestelltes Gerät unter Einstellungen → Du → Deine Geräte, sobald du
es nicht mehr brauchst.
Schritt 4: Den Server starten
Abschnitt betitelt „Schritt 4: Den Server starten“Auf Loopback, der Vorgabe und allem, was ein Client auf derselben Maschine braucht:
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030Um ihn von einer anderen Maschine zu erreichen, binde jede Schnittstelle. Ein
Bind außerhalb von Loopback ohne TLS wird beim Start abgelehnt (Refusing to bind non-loopback address … without TLS). Gib ihm deshalb ein Zertifikat:
./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/rpcOder terminiere TLS in einem Reverse-Proxy in einem vertrauenswürdigen privaten Netz und gib dahinter einen Klartext-Bind frei:
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030 \ --bind any --insecure --public-url wss://server.example/rpc--insecure wird ignoriert, wenn --tls-cert und --tls-key beide gesetzt
sind. TLS gewinnt immer. Der Server gibt den Klartext-Zustand als
insecure: true in GET /healthz bekannt, und Clients kennzeichnen so eine
Verbindung mit Unsicher.
Der Server läuft, bis SIGINT oder SIGTERM kommt, und fährt dann sauber
herunter.
Schritt 5: Einen Client verbinden
Abschnitt betitelt „Schritt 5: Einen Client verbinden“Richte einen Client auf die Server-URL und den Kopplungsschlüssel aus Schritt 3 aus:
- Desktop-App: Auf dem Bildschirm beim ersten Start (Wie soll Control Center laufen?) füllst du die Verbindungsfelder aus, statt In dieser App ausführen zu wählen, oder du fügst den Server später unter Einstellungen → Server → Verbindung und Status hinzu. Siehe Mit einem Remote-Server verbinden.
- Web-Client: Öffne app.usectrl.dev (oder deine eigene Kopie des Web-Builds) und gib Server-URL, Geräte-ID und Kopplungsschlüssel ein.
- Telefon: Kopple die App Remote von einem laufenden Client aus. Siehe Ein Gerät koppeln.
Schritt 6: Einen Google Calendar verbinden (optional)
Abschnitt betitelt „Schritt 6: Einen Google Calendar verbinden (optional)“Um den Kalender eines Arbeitsbereichs ohne angeschlossene Oberfläche zu synchronisieren, nutze den Gerätecode-Ablauf:
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>Er gibt einen Code und eine URL aus, die du auf einem anderen Gerät
genehmigst, speichert das Refresh-Token serverseitig und beendet sich. Ein
Build aus dem Quellcode bringt keinen Google-Client mit, deshalb sind die
Umgebungsvariablen für ID und Secret erforderlich. Ein Flag
--google-client-id gibt es nicht. Siehe
Einen Google Calendar verbinden.
Konfiguration
Abschnitt betitelt „Konfiguration“Die Flags oben sind die, die eine erste Einrichtung braucht. Die vollständige
Oberfläche, 23 Paare aus Flag und Umgebungsvariable, die vier Unterbefehle und
der Aufbau des Datenverzeichnisses, steht in der
cc_server-CLI-Referenz.
Die vier, die du am ehesten brauchst:
| Flag | Vorgabe | Bedeutung |
|---|---|---|
--data-dir |
das anwendungsbezogene Datenverzeichnis des Betriebssystems je Benutzer | Datenbanken, Geheimnisse, Modelle und zwischengespeicherte Medien |
--port |
9030 |
TCP-Port (0 = ephemer) |
--bind |
loopback |
loopback, oder any für jede Schnittstelle (braucht TLS oder --insecure) |
--public-url |
aus dem Bind abgeleitet | Die RPC-URL, die gekoppelten Clients bekannt gegeben wird. Setze sie hinter einem Proxy, NAT oder Tunnel ausdrücklich |
Das Vorgabe-Datenverzeichnis ist ~/Library/Application Support/control-center
unter macOS, %APPDATA%\control-center unter Windows und
$XDG_DATA_HOME/control-center (sonst ~/.local/share/control-center) auf den
übrigen Systemen. Ein auf das Arbeitsverzeichnis bezogenes .cc_server wird
nur genutzt, wenn sich kein Home- oder Anwendungsdatenverzeichnis auflösen
lässt.
Was der Server ausliefert
Abschnitt betitelt „Was der Server ausliefert“cc_server ist die ganze Produktoberfläche: der Datenweg für Repo-RPC und die
Abonnements, der MCP-Werkzeug-Endpunkt unter POST /mcp und GET /sse auf
demselben Port, eingehende Webhooks, die Proxys für Medien und Schriften, die
Callbacks für Single Sign-on, das Lease-Protokoll der Fleet, serverseitiges
Abrufen von RSS im 30-Minuten-Takt und die Vektorsuche (die Erweiterung
sqlite_vector ist als prozessweite Auto-Extension registriert und fällt nur
dort auf reine Volltextsuche zurück, wo die Erweiterung fehlt).
Der Dispatcher ist zustandslos. Eine Sitzung bindet keinen Arbeitsbereich.
Jeder arbeitsbereichsbezogene Aufruf trägt seine eigene workspace_id, und
über den Zugriff entscheiden Mitgliedschaft und Rolle des Aufrufers im
Arbeitsbereich. Ein Kopplungsschlüssel allein reicht dafür nicht.
Zwei Dinge, bevor du den Port über Loopback hinaus öffnest:
- MCP bleibt außerhalb des Hosts fail-closed. Eine Oberfläche ohne Token antwortet nur auf Loopback.
anyzu binden erweitert sie nicht. Eine Anfrage von außerhalb des Hosts wird abgelehnt mit “MCP requires a bearer token to answer non-loopback clients”, bis du eines konfigurierst (mcp.setTokenoder Einstellungen → Server → MCP-Server). GET /healthzist absichtlich ohne Authentifizierung, mit Wildcard-CORS, und veröffentlicht ID, Name, Identitätsfingerabdruck und Build-Version des Servers. Clients brauchen den Fingerabdruck, um den Server zu pinnen. Das ist so gewollt, und jeder erreichbarecc_servergibt genau das jedem, der fragt.
Verwandte Anleitungen
Abschnitt betitelt „Verwandte Anleitungen“- Mit einem Remote-Server verbinden
- Ein Gerät koppeln
- Einen Fleet-Worker betreiben
- Einen Google Calendar verbinden