Claude API Docker Containerization Guide
Containerizing an application that calls the Claude API means packaging your code, runtime, and dependencies into a Docker image that runs consistently in dev, CI, and production — without ever baking your API key into the image itself. This guide walks through building that image, handling secrets safely, and structuring a docker-compose.yml for local development and deployment.
The core challenge isn't Claude-specific — it's the same as containerizing any service that talks to an external HTTPS API: keep secrets out of the image, keep the container stateless, and make sure network egress and timeouts are configured for long-running or streaming requests. Below is a concrete, reproducible setup you can copy into a real project.
Project structure
A minimal Node.js service that proxies requests to Claude (or to an API-compatible layer like SubToAPI) typically looks like this:
app/
├── Dockerfile
├── docker-compose.yml
├── package.json
├── src/
│ └── server.js
└── .dockerignore
Keep node_modules, .env, and .git out of the build context with .dockerignore:
node_modules
.env
.git
*.log
Writing the Dockerfile
Use a multi-stage build to keep the final image small and avoid shipping build tools or dev dependencies into production.
# ---- build stage ----
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
# ---- runtime stage ----
FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app /app
EXPOSE 3000
USER node
CMD ["node", "src/server.js"]
Key points:
node:20-alpinekeeps the image small (under ~150MB in most cases).USER noderuns the process as a non-root user — important if your container ever gets exposed to the internet.- No API keys,
.envfiles, or secrets areCOPY'd into the image at any stage.
Handling the API key securely
Never hardcode ANTHROPIC_API_KEY or SUBTOAPI_KEY in the Dockerfile or source code. Pass it in at runtime as an environment variable:
docker run -d \
--name claude-service \
-e SUBTOAPI_KEY=sub_live_xxx \
-p 3000:3000 \
my-claude-app:latest
For local development, use a .env file that's excluded from version control and loaded via docker-compose.yml:
version: "3.9"
services:
app:
build: .
ports:
- "3000:3000"
env_file:
- .env
restart: unless-stopped
In production (Kubernetes, ECS, Cloud Run, etc.), inject the key via a secrets manager rather than a plain env var when possible — Docker itself doesn't encrypt environment variables at rest.
Example server code
A simple Express endpoint that calls Claude through SubToAPI's HTTPS interface — containerized the same way you'd containerize a call to the native Anthropic API:
import express from "express";
const app = express();
app.use(express.json());
app.post("/chat", async (req, res) => {
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 1024,
messages: req.body.messages
})
});
const data = await response.json();
res.json(data);
});
app.listen(3000, () => console.log("listening on 3000"));
This pattern works identically whether you're calling Anthropic directly or routing through SubToAPI, since both speak a compatible HTTPS request/response format. See /docs/messages for the full request schema.
Networking and timeout considerations
Containers add a network hop, so configure timeouts generously, especially for streaming responses:
- Set an HTTP client timeout of at least 60–120 seconds for non-streaming requests.
- For streaming, make sure your reverse proxy (nginx, Traefik, ALB) doesn't buffer or cut off chunked responses — disable proxy buffering for the relevant route.
- Keep
keep-aliveenabled on outbound connections to avoid repeated TLS handshakes on every request.
If you're building a streaming chat interface inside a container, check /docs/streaming for details on handling server-sent events correctly through a proxy layer.
Health checks and graceful shutdown
Add a HEALTHCHECK so orchestrators know when your container is ready to receive traffic:
HEALTHCHECK --interval=30s --timeout=5s \
CMD wget -qO- http://localhost:3000/health || exit 1
Implement /health as a lightweight endpoint that doesn't call Claude itself — you don't want a transient API outage to take your container out of the load balancer pool.
Handle SIGTERM in your app so in-flight requests (especially streaming ones) finish cleanly before the container stops:
process.on("SIGTERM", () => {
server.close(() => process.exit(0));
});
Scaling with multiple containers
Since the container itself holds no state and no secrets baked in, scaling horizontally is just a matter of running more replicas behind a load balancer, each pulling the API key from the same secrets source. This is also where centralizing key management pays off — if you're running the same key across dozens of containers, rotating it manually in every environment is error-prone. Tools like SubToAPI let you manage application keys, usage metadata, and team seats from one dashboard rather than tracking raw Anthropic credentials across every container and environment. See /pricing for plan details or /signup to try it.
Build and run checklist
- [ ] Multi-stage Dockerfile, no secrets in any layer
- [ ]
.dockerignoreexcludes.env,node_modules,.git - [ ] API key injected via
env_file,-e, or a secrets manager - [ ] Non-root
USERset in the final stage - [ ]
HEALTHCHECKdefined and independent of the Claude API - [ ] Reverse proxy configured to not buffer streaming responses
- [ ] Graceful shutdown handling for in-flight requests
Questions
Do I need to containerize the Claude API itself, or just my app? Just your app. Claude is a hosted API — there's nothing to run locally. You're containerizing the service that calls it, not Claude itself.
Should I store my API key in the Docker image? No. Never COPY or ARG secrets into an image layer, since they remain inspectable in the image history. Inject keys at runtime via environment variables or a secrets manager.
How do I test Claude API calls inside a container locally? Use docker-compose with an .env file holding a test key, mount your source with a volume for hot-reload during development, and call your container's exposed port with curl or Postman to verify responses before deploying.