From af87ddf8620f987a8af3eb86a91ed10be3b18c7d Mon Sep 17 00:00:00 2001 From: Austin Tucker <220209011+austinkennethtucker@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:47:52 -0400 Subject: [PATCH] Add Windows Docker setup --- README.md | 3 ++ docs/windows.md | 98 +++++++++++++++++++++++++++++++++++ scripts/setup_windows.ps1 | 106 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 docs/windows.md create mode 100644 scripts/setup_windows.ps1 diff --git a/README.md b/README.md index ff0ad64..3a9bb00 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,9 @@ curl -sS http://127.0.0.1:8320/healthz dedicated ChatGPT login. It is normal for the container to report unhealthy before device login is complete. +Windows users running Docker Desktop should use the PowerShell bootstrap and +platform notes in [`docs/windows.md`](docs/windows.md). + ## API Examples Load the wrapper bearer token into a shell variable: diff --git a/docs/windows.md b/docs/windows.md new file mode 100644 index 0000000..8560f2a --- /dev/null +++ b/docs/windows.md @@ -0,0 +1,98 @@ +# Windows Docker Setup + +This project runs on Windows through Docker Desktop Linux containers. It does +not use a Windows-native container image. + +## Requirements + +- Windows 10 or 11 with Docker Desktop. +- Docker Desktop configured for Linux containers with the WSL 2 backend. +- PowerShell 5.1 or newer. +- A dedicated Codex/ChatGPT sign-in for this project. +- Optional: `docker login ghcr.io` if the GitHub Container Registry package is + private. + +Keep the repository in a normal local path such as `C:\Users\\Projects`. +Avoid synced or network-backed folders for the `data/` directory if Docker file +sharing behaves oddly. + +## Setup + +From the repository root in PowerShell, run: + +```powershell +.\scripts\setup_windows.ps1 +``` + +The setup script: + +- copies `.env.example` to `.env` if `.env` does not exist; +- creates `data\codex-home`, `data\codex-work`, and `data\secrets`; +- writes a strong wrapper bearer token to `data\secrets\proxy_api_key` as UTF-8; +- leaves existing `.env` and token files in place unless `-ForceSecret` is + used. + +Then build and run locally: + +```powershell +docker compose up --build -d +``` + +For a published image, pass the image tag once during setup: + +```powershell +.\scripts\setup_windows.ps1 -Image "ghcr.io/subdepthtech/codex-cli-provider:v0.1.2" +docker compose -f docker-compose.image.yml pull +docker compose -f docker-compose.image.yml up -d +``` + +Do not use `latest`. + +## Codex Login + +Complete Codex login inside the running container: + +```powershell +docker exec -it codex-cli-provider ` + codex login --device-auth ` + -c 'forced_login_method="chatgpt"' ` + -c 'cli_auth_credentials_store="file"' +``` + +Credentials are written to `/root/.codex` in the Linux container, backed by the +local `data\codex-home` directory. + +Confirm readiness: + +```powershell +curl.exe -sS http://127.0.0.1:8320/healthz +``` + +## Client Settings + +Use these settings from Windows apps: + +- Base URL: `http://127.0.0.1:8320/v1` +- API key: the contents of `data\secrets\proxy_api_key` +- Model: `codex-cli-default` +- Concurrency: `1` + +Load the wrapper token into PowerShell for manual API checks: + +```powershell +$ProxyApiKey = (Get-Content -Raw data\secrets\proxy_api_key).Trim() +curl.exe -sS ` + -H "Authorization: Bearer $ProxyApiKey" ` + http://127.0.0.1:8320/v1/models +``` + +## Notes + +- Keep Docker Desktop in Linux-container mode. +- Do not set `OPENAI_API_KEY`. +- Keep Codex/ChatGPT auth in `data\codex-home`. +- Keep the wrapper bearer token in `data\secrets\proxy_api_key`. +- If `docker compose` cannot mount files, check Docker Desktop file-sharing + settings for the drive that contains the repository. +- If `data\secrets\proxy_api_key` was created manually, make sure it is UTF-8 + text and contains only the token plus an optional trailing newline. diff --git a/scripts/setup_windows.ps1 b/scripts/setup_windows.ps1 new file mode 100644 index 0000000..af4a32f --- /dev/null +++ b/scripts/setup_windows.ps1 @@ -0,0 +1,106 @@ +param( + [string]$Image = "", + [switch]$ForceSecret +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoRoot = Split-Path -Parent $ScriptDir +$Utf8NoBom = New-Object System.Text.UTF8Encoding -ArgumentList $false + +function Write-Utf8File { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string]$Content + ) + [IO.File]::WriteAllText($Path, $Content, $Utf8NoBom) +} + +function Set-DotEnvValue { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string]$Name, + [Parameter(Mandatory = $true)][string]$Value + ) + + $escapedName = [regex]::Escape($Name) + if (Test-Path -LiteralPath $Path) { + $lines = New-Object "System.Collections.Generic.List[string]" + foreach ($line in [IO.File]::ReadAllLines($Path)) { + $lines.Add($line) + } + } else { + $lines = New-Object "System.Collections.Generic.List[string]" + } + + $updated = $false + for ($i = 0; $i -lt $lines.Count; $i++) { + if ($lines[$i] -match "^\s*#?\s*$escapedName=") { + $lines[$i] = "$Name=$Value" + $updated = $true + } + } + + if (-not $updated) { + $lines.Add("$Name=$Value") + } + + [IO.File]::WriteAllLines($Path, $lines, $Utf8NoBom) +} + +Set-Location $RepoRoot + +$envExamplePath = Join-Path $RepoRoot ".env.example" +$envPath = Join-Path $RepoRoot ".env" +$codexHomePath = Join-Path $RepoRoot "data/codex-home" +$codexWorkPath = Join-Path $RepoRoot "data/codex-work" +$secretsPath = Join-Path $RepoRoot "data/secrets" +$proxySecretPath = Join-Path $secretsPath "proxy_api_key" + +if (-not (Test-Path -LiteralPath $envExamplePath)) { + throw "Missing .env.example in $RepoRoot" +} + +if (-not (Test-Path -LiteralPath $envPath)) { + Copy-Item -LiteralPath $envExamplePath -Destination $envPath + Write-Host "Created .env from .env.example" +} else { + Write-Host "Keeping existing .env" +} + +if ($Image.Trim()) { + Set-DotEnvValue -Path $envPath -Name "CODEX_CLI_PROVIDER_IMAGE" -Value $Image.Trim() + Write-Host "Set CODEX_CLI_PROVIDER_IMAGE in .env" +} + +New-Item -ItemType Directory -Force -Path $codexHomePath, $codexWorkPath, $secretsPath | Out-Null +Write-Host "Ensured data/codex-home, data/codex-work, and data/secrets exist" + +if ((Test-Path -LiteralPath $proxySecretPath) -and (-not $ForceSecret)) { + Write-Host "Keeping existing data/secrets/proxy_api_key" +} else { + $tokenBytes = New-Object byte[] 48 + $rng = [Security.Cryptography.RandomNumberGenerator]::Create() + try { + $rng.GetBytes($tokenBytes) + } finally { + $rng.Dispose() + } + $token = [Convert]::ToBase64String($tokenBytes) + Write-Utf8File -Path $proxySecretPath -Content "$token`n" + Write-Host "Wrote new UTF-8 wrapper bearer token to data/secrets/proxy_api_key" +} + +Write-Host "" +Write-Host "Next steps:" +if ($Image.Trim()) { + Write-Host " docker compose -f docker-compose.image.yml pull" + Write-Host " docker compose -f docker-compose.image.yml up -d" +} else { + Write-Host " docker compose up --build -d" +} +Write-Host " docker exec -it codex-cli-provider codex login --device-auth -c 'forced_login_method=`"chatgpt`"' -c 'cli_auth_credentials_store=`"file`"'" +Write-Host "" +Write-Host "Use http://127.0.0.1:8320/v1 as the client base URL."