Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.git
.gitignore
node_modules
npm-debug.log
dist
coverage
*.log
*.swp
46 changes: 46 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# syntax=docker/dockerfile:1.6

FROM ubuntu:24.04 AS base
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl gnupg \
&& curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

FROM base AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build

FROM base AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev \
&& npm install -g @anthropic-ai/claude-code
COPY --from=build /app/dist ./dist
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh \
&& if ! getent group 1000 >/dev/null; then groupadd -g 1000 claudeproxy; fi \
&& if ! id -u claudeproxy >/dev/null 2>&1; then useradd -m -u 1000 -g 1000 -o -s /bin/bash claudeproxy; fi \
&& mkdir -p /data \
&& chown -R 1000:1000 /data

ENV HOME=/data \
XDG_CONFIG_HOME=/data/.config \
XDG_DATA_HOME=/data/.local/share \
XDG_STATE_HOME=/data/.local/state \
XDG_CACHE_HOME=/data/.cache \
HOST=0.0.0.0 \
PORT=3456

VOLUME ["/data"]
USER claudeproxy
EXPOSE 3456
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD []
111 changes: 111 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,36 @@ response = client.chat.completions.create(
)
```

### OpenClaw (Docker)

When running this provider in Docker, point OpenClaw at the host IP and mapped port:

```json
{
"models": {
"mode": "merge",
"providers": {
"claude-proxy": {
"baseUrl": "http://<ip>:3456/v1",
"apiKey": "proxy",
"api": "openai-completions",
"models": [
{
"id": "claude-sonnet-4",
"name": "claude-sonnet-4",
"reasoning": false,
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 200000,
"maxTokens": 8192
}
]
}
}
}
}
```

## Auto-Start on macOS

Create a LaunchAgent to start the provider automatically on login. See `docs/macos-setup.md` for detailed instructions.
Expand Down Expand Up @@ -204,6 +234,87 @@ src/

If you're already paying for Claude Max, this provider lets you use that subscription for API-style access at no additional cost.

## Docker (Recommended)

This project ships with a production-ready Docker image that includes Claude CLI and keeps all data, config, and auth secrets on a persistent volume. The container itself remains stateless and safe to replace.

### Build

```bash
docker build -t claude-max-api-proxy:latest .
```

### One-time Claude CLI login (persists on volume)

Claude CLI uses OAuth. You must run the login once, then the credentials are stored on the volume and survive container upgrades.

```bash
docker run --rm -it \
-v claudeproxy_data:/data \
claude-max-api-proxy:latest \
claude auth login
```

### Run

```bash
docker run -d --name claudeproxy \
-p 3456:3456 \
-v claudeproxy_data:/data \
claude-max-api-proxy:latest
```

### Custom Port

```bash
docker run -d --name claudeproxy \
-e PORT=8080 -p 8080:8080 \
-v claudeproxy_data:/data \
claude-max-api-proxy:latest
```

### Docker Compose

```bash
docker compose up -d --build
```

Edit `docker-compose.yml` to change the exposed port.

### Where data, config, and secrets live

All runtime state is redirected to `/data` inside the container and should be mounted to a persistent volume:

- `HOME=/data`
- `XDG_CONFIG_HOME=/data/.config`
- `XDG_DATA_HOME=/data/.local/share`
- `XDG_STATE_HOME=/data/.local/state`
- `XDG_CACHE_HOME=/data/.cache`

This ensures:
- Claude CLI OAuth secrets persist across container updates.
- Session mappings (`.claude-code-cli-sessions.json`) persist.
- The container filesystem stays stateless.

### Logs

By default, logs go to stdout/stderr (preferred for container environments and avoids disk growth).
If you want file logs on the volume:

```bash
docker run -d --name claudeproxy \
-e LOG_DIR=/data/logs \
-p 3456:3456 \
-v claudeproxy_data:/data \
claude-max-api-proxy:latest
```

### Health Check

```bash
curl http://localhost:3456/health
```

## Troubleshooting

### "Claude CLI not found"
Expand Down
20 changes: 20 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
claudeproxy:
build: .
image: claude-max-api-proxy:latest
environment:
PORT: "3456"
HOST: "0.0.0.0"
HOME: "/data"
XDG_CONFIG_HOME: "/data/.config"
XDG_DATA_HOME: "/data/.local/share"
XDG_STATE_HOME: "/data/.local/state"
XDG_CACHE_HOME: "/data/.cache"
# LOG_DIR: "/data/logs" # Optional: write logs to a file on the volume
volumes:
- claudeproxy_data:/data
ports:
- "3456:3456"

volumes:
claudeproxy_data:
29 changes: 29 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail

DATA_DIR="${DATA_DIR:-/data}"

XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$DATA_DIR/.config}"
XDG_DATA_HOME="${XDG_DATA_HOME:-$DATA_DIR/.local/share}"
XDG_STATE_HOME="${XDG_STATE_HOME:-$DATA_DIR/.local/state}"
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$DATA_DIR/.cache}"

export XDG_CONFIG_HOME XDG_DATA_HOME XDG_STATE_HOME XDG_CACHE_HOME

mkdir -p \
"$DATA_DIR" \
"$XDG_CONFIG_HOME" \
"$XDG_DATA_HOME" \
"$XDG_STATE_HOME" \
"$XDG_CACHE_HOME"

if [ "$#" -eq 0 ]; then
set -- node dist/server/standalone.js
fi

if [ -n "${LOG_DIR:-}" ] && [ "$1" = "node" ] && [ "${2:-}" = "dist/server/standalone.js" ]; then
mkdir -p "$LOG_DIR"
exec "$@" >>"$LOG_DIR/claudeproxy.log" 2>&1
fi

exec "$@"
14 changes: 10 additions & 4 deletions src/server/standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,19 @@ async function main(): Promise<void> {
console.log("Claude Code CLI Provider - Standalone Server");
console.log("============================================\n");

// Parse port from command line
const port = parseInt(process.argv[2] || String(DEFAULT_PORT), 10);
// Parse port from command line or env
const portInput =
process.argv[2] || process.env.PORT || String(DEFAULT_PORT);
const port = parseInt(portInput, 10);
if (isNaN(port) || port < 1 || port > 65535) {
console.error(`Invalid port: ${process.argv[2]}`);
console.error(`Invalid port: ${portInput}`);
process.exit(1);
}

// Bind host (useful for containers)
const host =
process.env.BIND_HOST || process.env.HOST || "127.0.0.1";

// Verify Claude CLI
console.log("Checking Claude CLI...");
const cliCheck = await verifyClaude();
Expand All @@ -45,7 +51,7 @@ async function main(): Promise<void> {

// Start server
try {
await startServer({ port });
await startServer({ port, host });
console.log("\nServer ready. Test with:");
console.log(` curl -X POST http://localhost:${port}/v1/chat/completions \\`);
console.log(` -H "Content-Type: application/json" \\`);
Expand Down