Skip to content

Commit af87ddf

Browse files
Add Windows Docker setup
1 parent 8b9b154 commit af87ddf

3 files changed

Lines changed: 207 additions & 0 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ curl -sS http://127.0.0.1:8320/healthz
171171
dedicated ChatGPT login. It is normal for the container to report unhealthy
172172
before device login is complete.
173173

174+
Windows users running Docker Desktop should use the PowerShell bootstrap and
175+
platform notes in [`docs/windows.md`](docs/windows.md).
176+
174177
## API Examples
175178

176179
Load the wrapper bearer token into a shell variable:

docs/windows.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Windows Docker Setup
2+
3+
This project runs on Windows through Docker Desktop Linux containers. It does
4+
not use a Windows-native container image.
5+
6+
## Requirements
7+
8+
- Windows 10 or 11 with Docker Desktop.
9+
- Docker Desktop configured for Linux containers with the WSL 2 backend.
10+
- PowerShell 5.1 or newer.
11+
- A dedicated Codex/ChatGPT sign-in for this project.
12+
- Optional: `docker login ghcr.io` if the GitHub Container Registry package is
13+
private.
14+
15+
Keep the repository in a normal local path such as `C:\Users\<you>\Projects`.
16+
Avoid synced or network-backed folders for the `data/` directory if Docker file
17+
sharing behaves oddly.
18+
19+
## Setup
20+
21+
From the repository root in PowerShell, run:
22+
23+
```powershell
24+
.\scripts\setup_windows.ps1
25+
```
26+
27+
The setup script:
28+
29+
- copies `.env.example` to `.env` if `.env` does not exist;
30+
- creates `data\codex-home`, `data\codex-work`, and `data\secrets`;
31+
- writes a strong wrapper bearer token to `data\secrets\proxy_api_key` as UTF-8;
32+
- leaves existing `.env` and token files in place unless `-ForceSecret` is
33+
used.
34+
35+
Then build and run locally:
36+
37+
```powershell
38+
docker compose up --build -d
39+
```
40+
41+
For a published image, pass the image tag once during setup:
42+
43+
```powershell
44+
.\scripts\setup_windows.ps1 -Image "ghcr.io/subdepthtech/codex-cli-provider:v0.1.2"
45+
docker compose -f docker-compose.image.yml pull
46+
docker compose -f docker-compose.image.yml up -d
47+
```
48+
49+
Do not use `latest`.
50+
51+
## Codex Login
52+
53+
Complete Codex login inside the running container:
54+
55+
```powershell
56+
docker exec -it codex-cli-provider `
57+
codex login --device-auth `
58+
-c 'forced_login_method="chatgpt"' `
59+
-c 'cli_auth_credentials_store="file"'
60+
```
61+
62+
Credentials are written to `/root/.codex` in the Linux container, backed by the
63+
local `data\codex-home` directory.
64+
65+
Confirm readiness:
66+
67+
```powershell
68+
curl.exe -sS http://127.0.0.1:8320/healthz
69+
```
70+
71+
## Client Settings
72+
73+
Use these settings from Windows apps:
74+
75+
- Base URL: `http://127.0.0.1:8320/v1`
76+
- API key: the contents of `data\secrets\proxy_api_key`
77+
- Model: `codex-cli-default`
78+
- Concurrency: `1`
79+
80+
Load the wrapper token into PowerShell for manual API checks:
81+
82+
```powershell
83+
$ProxyApiKey = (Get-Content -Raw data\secrets\proxy_api_key).Trim()
84+
curl.exe -sS `
85+
-H "Authorization: Bearer $ProxyApiKey" `
86+
http://127.0.0.1:8320/v1/models
87+
```
88+
89+
## Notes
90+
91+
- Keep Docker Desktop in Linux-container mode.
92+
- Do not set `OPENAI_API_KEY`.
93+
- Keep Codex/ChatGPT auth in `data\codex-home`.
94+
- Keep the wrapper bearer token in `data\secrets\proxy_api_key`.
95+
- If `docker compose` cannot mount files, check Docker Desktop file-sharing
96+
settings for the drive that contains the repository.
97+
- If `data\secrets\proxy_api_key` was created manually, make sure it is UTF-8
98+
text and contains only the token plus an optional trailing newline.

scripts/setup_windows.ps1

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
param(
2+
[string]$Image = "",
3+
[switch]$ForceSecret
4+
)
5+
6+
Set-StrictMode -Version Latest
7+
$ErrorActionPreference = "Stop"
8+
9+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
10+
$RepoRoot = Split-Path -Parent $ScriptDir
11+
$Utf8NoBom = New-Object System.Text.UTF8Encoding -ArgumentList $false
12+
13+
function Write-Utf8File {
14+
param(
15+
[Parameter(Mandatory = $true)][string]$Path,
16+
[Parameter(Mandatory = $true)][string]$Content
17+
)
18+
[IO.File]::WriteAllText($Path, $Content, $Utf8NoBom)
19+
}
20+
21+
function Set-DotEnvValue {
22+
param(
23+
[Parameter(Mandatory = $true)][string]$Path,
24+
[Parameter(Mandatory = $true)][string]$Name,
25+
[Parameter(Mandatory = $true)][string]$Value
26+
)
27+
28+
$escapedName = [regex]::Escape($Name)
29+
if (Test-Path -LiteralPath $Path) {
30+
$lines = New-Object "System.Collections.Generic.List[string]"
31+
foreach ($line in [IO.File]::ReadAllLines($Path)) {
32+
$lines.Add($line)
33+
}
34+
} else {
35+
$lines = New-Object "System.Collections.Generic.List[string]"
36+
}
37+
38+
$updated = $false
39+
for ($i = 0; $i -lt $lines.Count; $i++) {
40+
if ($lines[$i] -match "^\s*#?\s*$escapedName=") {
41+
$lines[$i] = "$Name=$Value"
42+
$updated = $true
43+
}
44+
}
45+
46+
if (-not $updated) {
47+
$lines.Add("$Name=$Value")
48+
}
49+
50+
[IO.File]::WriteAllLines($Path, $lines, $Utf8NoBom)
51+
}
52+
53+
Set-Location $RepoRoot
54+
55+
$envExamplePath = Join-Path $RepoRoot ".env.example"
56+
$envPath = Join-Path $RepoRoot ".env"
57+
$codexHomePath = Join-Path $RepoRoot "data/codex-home"
58+
$codexWorkPath = Join-Path $RepoRoot "data/codex-work"
59+
$secretsPath = Join-Path $RepoRoot "data/secrets"
60+
$proxySecretPath = Join-Path $secretsPath "proxy_api_key"
61+
62+
if (-not (Test-Path -LiteralPath $envExamplePath)) {
63+
throw "Missing .env.example in $RepoRoot"
64+
}
65+
66+
if (-not (Test-Path -LiteralPath $envPath)) {
67+
Copy-Item -LiteralPath $envExamplePath -Destination $envPath
68+
Write-Host "Created .env from .env.example"
69+
} else {
70+
Write-Host "Keeping existing .env"
71+
}
72+
73+
if ($Image.Trim()) {
74+
Set-DotEnvValue -Path $envPath -Name "CODEX_CLI_PROVIDER_IMAGE" -Value $Image.Trim()
75+
Write-Host "Set CODEX_CLI_PROVIDER_IMAGE in .env"
76+
}
77+
78+
New-Item -ItemType Directory -Force -Path $codexHomePath, $codexWorkPath, $secretsPath | Out-Null
79+
Write-Host "Ensured data/codex-home, data/codex-work, and data/secrets exist"
80+
81+
if ((Test-Path -LiteralPath $proxySecretPath) -and (-not $ForceSecret)) {
82+
Write-Host "Keeping existing data/secrets/proxy_api_key"
83+
} else {
84+
$tokenBytes = New-Object byte[] 48
85+
$rng = [Security.Cryptography.RandomNumberGenerator]::Create()
86+
try {
87+
$rng.GetBytes($tokenBytes)
88+
} finally {
89+
$rng.Dispose()
90+
}
91+
$token = [Convert]::ToBase64String($tokenBytes)
92+
Write-Utf8File -Path $proxySecretPath -Content "$token`n"
93+
Write-Host "Wrote new UTF-8 wrapper bearer token to data/secrets/proxy_api_key"
94+
}
95+
96+
Write-Host ""
97+
Write-Host "Next steps:"
98+
if ($Image.Trim()) {
99+
Write-Host " docker compose -f docker-compose.image.yml pull"
100+
Write-Host " docker compose -f docker-compose.image.yml up -d"
101+
} else {
102+
Write-Host " docker compose up --build -d"
103+
}
104+
Write-Host " docker exec -it codex-cli-provider codex login --device-auth -c 'forced_login_method=`"chatgpt`"' -c 'cli_auth_credentials_store=`"file`"'"
105+
Write-Host ""
106+
Write-Host "Use http://127.0.0.1:8320/v1 as the client base URL."

0 commit comments

Comments
 (0)