Docker Compose is the supported way to run CLI2API. Published images, the console System-page updater, pre-update SQLite snapshots, and automatic rollback all assume this single-container install. A source checkout of Go / Node is for development; it does not receive managed updates.
The deployment is one container containing the Go control plane, provider
adapters, Node runtime, pinned qodercli, and frontend. SQLite and account
credentials persist in the qoder-data volume; per-account ephemeral runtimes
use tmpfs where the provider requires them.
From the repository root, use:
macOS / Linux:
./scripts/start.shWindows PowerShell:
powershell -ExecutionPolicy Bypass -File .\scripts\start.ps1Both launchers create deploy/.env if needed, pull the published image, fall
back to a local build when necessary, and wait for /health.
To run Compose directly:
docker compose --env-file deploy/.env -f deploy/docker-compose.yml up -dAdd --build to build from the checked-out source. Only 127.0.0.1:3010 is
published. Account and credential operations require the key stored in SQLite.
After startup, add accounts from /accounts using the login methods supported by
each provider: Qoder browser OAuth, PAT, or qoder-native-v1 import; WorkBuddy /
Trae browser OAuth or their provider credential import where applicable. Provider
credentials are stored in SQLite. Qoder workers materialize
their auth files only under the per-account tmpfs runtime directory; in-process
providers keep their adapter state in the Go process.
For another container on the same Docker network:
base_url = http://qoder-api-proxy:3010/v1
api_key = <same key printed on first startup>
export CLI2API_API_KEY='paste-the-key-printed-on-first-start'
curl http://127.0.0.1:3010/v1/chat/completions \
-H "Authorization: Bearer $CLI2API_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "<model-id-from-v1-models>",
"messages": [{"role": "user", "content": "Reply with OK only"}],
"stream": false
}'PowerShell equivalent:
$env:CLI2API_API_KEY = "paste-the-key-printed-on-first-start"
$Headers = @{ Authorization = "Bearer $env:CLI2API_API_KEY" }
$Body = @{
model = "<model-id-from-v1-models>"
messages = @(@{ role = "user"; content = "Reply with OK only" })
stream = $false
} | ConvertTo-Json -Depth 4
Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:3010/v1/chat/completions" -Headers $Headers -ContentType "application/json" -Body $BodyPin a request to a specific account with the X-Qoder-Account: acc_... header
(a historical header name that applies to every provider). Add X-CLI2API-Session
when consecutive requests should prefer the same account.
| Variable | Default | Purpose |
|---|---|---|
QODER_DATA_DIR |
/data |
SQLite database and durable account credentials |
QODER_RUNTIME_DIR |
/run/cli2api |
Ephemeral per-account runtime homes for providers that use child processes |
QODER_MAX_RETRY_ACCOUNTS |
4 |
Maximum accounts attempted for one request (1-64) |
QODER_WORKER_BASE_PORT |
32100 |
Internal child-runtime port range |
QODERCLI_JS |
image default | Pinned Qoder Global CLI bundle |
QODERCNCLI_JS |
image default | Pinned Qoder CN CLI bundle |
UPDATE_GITHUB_TOKEN |
empty | Optional GitHub token for release checks |
UPDATE_AGENT_URL |
empty | Docker Desktop host updater URL, written by the installer |
UPDATE_AGENT_TOKEN |
empty | Docker Desktop updater token, written by the installer |
CLI2API_UPDATER_SOCKET_DIR |
platform-specific | Host directory mounted read-only for the Linux updater socket |
Per-account concurrency is configured through max_inflight in the console; it is
persisted with each account and passed to its runtime.
The API key is generated once and stored in SQLite. There is no environment-variable bootstrap path; changing container environment variables does not replace the stored key.
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Health probe; no API key required |
GET |
/v1/models |
Model catalog |
POST |
/v1/chat/completions |
OpenAI-compatible chat |
POST |
/v1/messages |
Anthropic-compatible messages |
POST |
/v1/responses |
OpenAI Responses-compatible API |
GET/POST/PATCH/DELETE |
/api/* |
Console management API |
All console and API routes except /health require the API key stored in SQLite.
Start qoder-api-proxy once before installing the optional host updater.
| Host | Container platform | Updater asset |
|---|---|---|
| Linux x86-64 | linux/amd64 |
cli2api-updater_linux_amd64 |
| Linux ARM64 | linux/arm64 |
cli2api-updater_linux_arm64 |
| macOS Intel | Docker Desktop linux/amd64 |
cli2api-updater_darwin_amd64 |
| macOS Apple Silicon | Docker Desktop linux/arm64 |
cli2api-updater_darwin_arm64 |
| Windows x86-64 | Docker Desktop Linux containers | cli2api-updater_windows_amd64.exe |
| Windows ARM64 | Docker Desktop Linux containers | cli2api-updater_windows_arm64.exe |
Release assets include a SHA256 manifest. Installers use a verified prebuilt
updater whenever possible. They download the latest GitHub updater asset
first, then the asset matching the running container, then copy
/app/cli2api-updater from the container, and finally fall back to a local
Go 1.25.6+ build. A managed console update stages the host binary from the
target release, copies /app/cli2api-updater from the new Linux container when
possible, then atomically replaces the running updater and exits so systemd or
LaunchAgent starts the new process.
macOS + Docker Desktop:
./deploy/install-updater.sh
docker compose --env-file deploy/.env -f deploy/docker-compose.yml up -d --force-recreate qoder-api-proxyLinux + systemd:
sudo ./deploy/install-updater.sh
docker compose --env-file deploy/.env -f deploy/docker-compose.yml up -d --force-recreate qoder-api-proxyWindows + Docker Desktop in Linux-container mode, from the logged-in Docker user's PowerShell:
powershell -ExecutionPolicy Bypass -File .\deploy\install-updater.ps1
docker compose --env-file deploy\.env -f deploy\docker-compose.yml up -d --force-recreate qoder-api-proxyThe application container never receives the Docker socket. Linux uses a private
Unix Socket. macOS runs a per-user LaunchAgent and Windows runs a current-user
Scheduled Task; both Docker Desktop platforms use an authenticated updater bound
to 127.0.0.1 and reached through host.docker.internal.
Before replacement, the Go process pauses new API requests, waits for active
requests to drain, and creates a verified SQLite snapshot in /data/backups.
The updater recreates only qoder-api-proxy; it never runs
docker compose down -v, and it verifies that the same /data mount remains
attached. If the new version fails its versioned health check, or the host-updater binary
cannot be replaced, the updater restores the previous image and the pre-update
SQLite snapshot, discards any staged .new host binary, then pins
CLI2API_IMAGE to the previous version for future restarts. The five most recent
snapshots are retained.
The updater remains unavailable for development builds without a semantic version. The System page updates directly to the latest stable release, lists the intermediate versions it passes over, and can roll back to one of the three previous stable releases. An older host updater that does not yet speak staged updates still completes one jump, then replaces itself.
Release updater assets and the /app/cli2api-updater binary inside the image
are stamped with the same Version and Commit as the application. Status
reports that version next to protocol 2.
The updater API currently reports protocol version 2. Protocol 1 remains
accepted for staged-update hosts; unknown versions fail closed.
scripts/start.shanddeploy/install-updater.share for macOS/Linux.scripts/start.ps1anddeploy/install-updater.ps1are the Windows equivalents.- Published application images are Linux-only and multi-architecture; macOS and Windows run them through Docker Desktop.
- Published host updater assets cover Linux, macOS, and Windows on
amd64andarm64. - Run the Windows updater installer as the same signed-in user that runs Docker Desktop; do not install it as
LocalSystem. - Keep
deploy/.envprivate because Docker Desktop mode stores the updater token there. - Do not remove
qoder-dataunless you intentionally want to delete SQLite accounts and credentials.