ProxyCodeClaudePrice claude vs Our Uncensored Proxy API
Claude Code Proxy in Docker: Compose File and Settings
A claude code proxy docker setup puts the translation layer in a container, so Claude Code on your host can talk to an OpenAI-compatible API without installing Python tools locally. Below is a working Compose file, the config it mounts and the settings that break most often.
Last updated

Summary
- One container runs LiteLLM; Claude Code on the host points at localhost:4000.
- Keys live in a .env file that Compose reads, never in the image or the YAML.
- Bind the port to 127.0.0.1 so the proxy is not exposed to your network.
- Inside a container, localhost is the container itself; use the service name instead.
The layout
Create a project folder with three files:
claude-proxy/
docker-compose.yml
config.yaml
.env
The container runs a gateway that accepts Anthropic-style calls from Claude Code and forwards them in OpenAI format to https://api.proxycodeclaude.com/v1. The API serves one model, ID uncensored. Get a key on the API key page; new accounts can claim $0.50 of trial credit, which is plenty to test this setup.
The environment file
Compose reads .env from the project folder automatically. Put both secrets there:
BACKEND_KEY=YOUR_KEY
LITELLM_MASTER_KEY=sk-local-change-me
BACKEND_KEY is the key for the API. LITELLM_MASTER_KEY is a password you invent; Claude Code will present it to the proxy. Add .env to .gitignore. If the real key ever leaks, reissue it on the key page at any time. The old key stops working at once, and you only have to edit this one line and restart.
The proxy config
Save this as config.yaml. It is mounted read-only into the container:
model_list:
- model_name: uncensored
litellm_params:
model: openai/uncensored
api_base: https://api.proxycodeclaude.com/v1
api_key: os.environ/BACKEND_KEY
max_parallel_requests: 8
litellm_settings:
drop_params: true
num_retries: 2
The master key is not in this file. LiteLLM is documented to read the LITELLM_MASTER_KEY environment variable on its own; check the current LiteLLM docs if your version behaves differently. For a trial key, which allows 2 concurrent requests and 60 per minute, change max_parallel_requests to 2.
The Compose file
services:
claude-proxy:
image: ghcr.io/berriai/litellm:main-stable
command: ["--config", "/app/config.yaml", "--port", "4000"]
ports:
- "127.0.0.1:4000:4000"
volumes:
- ./config.yaml:/app/config.yaml:ro
environment:
BACKEND_KEY: ${BACKEND_KEY}
LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}
restart: unless-stopped
healthcheck:
test:
- CMD
- python
- -c
- "import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness')"
interval: 30s
timeout: 5s
retries: 3
A few choices worth explaining:
- Image tag.
main-stablemoves. Once your setup works, replace it with a specific version tag so a pull never changes behaviour under you. Check the LiteLLM project for current tags and for the image name, since registry paths and tag names can change. - Port binding.
127.0.0.1:4000:4000publishes the port to your machine only. Writing just4000:4000would expose it on every interface of the host. - Health check. It uses Python from the image, so it does not depend on curl being installed. If your version of the image names the endpoint differently, check the LiteLLM health documentation.
- Restart policy.
unless-stoppedbrings the proxy back after a reboot.
Start it and test it
docker compose up -d
docker compose logs -f claude-proxy
When the log shows the server listening, send a test message using the Anthropic-style endpoint, which is what Claude Code will call:
curl http://localhost:4000/v1/messages \
-H "x-api-key: sk-local-change-me" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model":"uncensored","max_tokens":100,"messages":[{"role":"user","content":"Reply with ready."}]}'
Then launch Claude Code on the host with the proxy as its endpoint:
export ANTHROPIC_BASE_URL="http://localhost:4000"
export ANTHROPIC_AUTH_TOKEN="sk-local-change-me"
export ANTHROPIC_MODEL="uncensored"
claude
Claude Code also has a setting for a smaller background model. Its variable name has changed over versions, so set it to uncensored using the name in the current docs, or the proxy will see a model name it does not list.
Running Claude Code inside Docker too
If you run Claude Code in a dev container, it must reach the proxy over the Compose network. Put both services in the same Compose project and use the service name as host:
export ANTHROPIC_BASE_URL="http://claude-proxy:4000"
Using localhost there fails, because inside the dev container it refers to the dev container itself. When the proxy runs on the host and the client runs in a container, use host.docker.internal on Docker Desktop, or add extra_hosts: ["host.docker.internal:host-gateway"] to the client service on Linux. Do not remove the 127.0.0.1 binding to make this work; the Compose network route is safer.
Confirm the container is healthy
The health check in the Compose file runs on its own, but you should know how to read it. Run docker compose ps a minute after start: the status column should show the service as healthy, not just running. If it stays in a starting state, the probe is failing, and the log will usually say why. The usual causes are a typo in config.yaml, an empty BACKEND_KEY, or a changed health endpoint name in your image version.
The container does no model work itself. The model runs on the API side, so the proxy needs very little memory or CPU, and a small laptop or a cheap server is enough. What it does need is a clean network path to https://api.proxycodeclaude.com/v1. If the container cannot resolve or reach that host, requests fail with an upstream error even though the container looks healthy. Test it from inside:
docker compose exec claude-proxy python -c "import urllib.request as u; print(u.urlopen('https://api.proxycodeclaude.com/v1/models').status)"
That call has no key, so a 401 error from the API is the expected answer and proves the route works. A timeout or a name-resolution error points at Docker networking, a corporate proxy or a firewall rule, not at LiteLLM or at your key.
One more habit worth having: keep a second, working copy of config.yaml and .env.example with the key blanked out. When a change breaks the setup, you can diff against a known good file instead of guessing.
Updating, logs and cleanup
Changing the key or the config is a restart, not a rebuild. After editing .env or config.yaml, run:
docker compose up -d --force-recreate
docker compose logs --tail 50 claude-proxy
To move to a newer image, pull it first and then recreate the container. Do this between work sessions, not in the middle of one, because a restart drops any stream in progress. Keep the log level at its default on shared machines; verbose debug output from a gateway may include request content, and prompts are better kept out of log files.
When you are done with the setup, docker compose down stops and removes the container and leaves your three files untouched. Because the key lives only in .env, you can delete that file and be sure nothing secret remains in the project. The API keeps your balance on the account, so nothing is lost when the container goes away.
Troubleshooting table
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 from the proxy | Token does not match the master key | Align ANTHROPIC_AUTH_TOKEN with LITELLM_MASTER_KEY, then restart the container |
| Error mentioning the upstream | Empty or wrong BACKEND_KEY | Check .env; test the key with a direct call to https://api.proxycodeclaude.com/v1/chat/completions |
429 with code concurrency | More than 8 requests at once on a full key (2 on trial) | Lower max_parallel_requests; a stream holds its slot until it ends |
429 with code rate_limited | Over 300 requests per minute (60 on trial) | Slow down or wait |
429 with Retry-After: 5 | Temporary overload | Retry after five seconds; num_retries helps |
| Config change has no effect | Container still has the old file loaded | docker compose restart claude-proxy |
| Replies arrive in one block | A reverse proxy in front is buffering | Turn buffering off for the route |
Remember what the backend is: one open-weight model, not Claude. Tool use and long agent loops behave differently, and the context window is 100,000 tokens. Costs are $0.15 per 1M input tokens and $0.50 per 1M output tokens; the figures in Price claude vs Our Uncensored Proxy API and on the pricing page show what that means per session. The documentation lists every request field.
More guides
Common questions
Do I need Docker to proxy Claude Code?
No. You can run the same gateway with pip on the host. Docker keeps Python dependencies out of your system and makes the setup easy to repeat.
Where should the API key go in a Compose setup?
In a .env file next to docker-compose.yml, passed to the container as an environment variable and read by the config as os.environ/BACKEND_KEY. Keep the file out of git.
Why can my dev container not reach localhost:4000?
Inside a container localhost is the container itself. Use the Compose service name, such as claude-proxy, or host.docker.internal if the proxy runs on the host.
Can the container run more than one backend?
Yes, by adding more entries to model_list. This API provides one model with the ID uncensored, so each entry would point at a different provider.
Generate your key now
Sign up, copy your API key, and update the base URL. That is all you need.
Open account