Claude API Docker Container Setup Tutorial
Running a service that calls the Claude API inside a Docker container is mostly about getting three things right: a small, reproducible image, secure handling of your API key, and a runtime that restarts cleanly in production. This tutorial walks through a working setup from an empty folder to a running container, using a Node.js example that generalizes easily to Python or Go.
If you just want a quick answer: build a minimal Node (or Python) image, install only production dependencies, pass your API key as an environment variable at runtime (never bake it into the image), expose a single port, and add a health check endpoint so your orchestrator knows the container is alive. The rest of this guide covers each step in detail, plus a docker-compose file for local development.
Prerequisites
- Docker 24+ installed locally
- Node.js 20+ if you want to test outside the container first
- An Anthropic API key, or a SubToAPI key if you're routing requests through a managed proxy (
sub_live_...) - Basic familiarity with environment variables and
.envfiles
Step 1: Project structure
Keep the project minimal so the Docker build context stays small:
claude-api-service/
├── Dockerfile
├── .dockerignore
├── docker-compose.yml
├── package.json
├── server.js
└── .env.example
Step 2: The application code
A minimal Express server that calls Claude and exposes a health check:
// server.js
import express from "express";
const app = express();
app.use(express.json());
const API_KEY = process.env.CLAUDE_API_KEY;
const API_URL = process.env.CLAUDE_API_URL || "https://api.anthropic.com/v1/messages";
app.get("/healthz", (req, res) => res.status(200).send("ok"));
app.post("/ask", async (req, res) => {
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": API_KEY,
"anthropic-version": "2023-06-01"
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 1024,
messages: [{ role: "user", content: req.body.prompt }]
})
});
const data = await response.json();
res.status(response.status).json(data);
});
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`listening on ${port}`));
If you're using SubToAPI instead of calling Anthropic directly, swap the URL and headers — the same container setup works unchanged, since SubToAPI exposes a compatible HTTPS endpoint at https://api.subtoapi.app/v1/messages with a Bearer token (Authorization: Bearer $SUBTOAPI_KEY) instead of an x-api-key header. See the quickstart for the exact request shape.
Step 3: Dockerfile
Use a multi-stage build to keep the final image small and avoid shipping dev dependencies:
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
COPY server.js ./
ENV NODE_ENV=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:3000/healthz || exit 1
USER node
CMD ["node", "server.js"]
Key points worth noting:
- Alpine base keeps the image under ~150MB, which matters for cold-start speed if you're deploying to ECS, Cloud Run, or similar.
USER nodeavoids running the process as root inside the container.HEALTHCHECKlets Docker (and most orchestrators) detect a hung process and restart it automatically.
Add a .dockerignore so node_modules, .env, and git metadata never end up in the build context:
node_modules
.env
.git
*.log
Step 4: Handling the API key securely
Never hardcode the key in the Dockerfile or commit a .env file. Instead:
# .env.example — commit this, not .env
CLAUDE_API_KEY=
PORT=3000
Locally, pass the real value at runtime:
docker build -t claude-api-service .
docker run -p 3000:3000 \
-e CLAUDE_API_KEY=$CLAUDE_API_KEY \
claude-api-service
In production, inject the key via your platform's secrets manager (AWS Secrets Manager, GCP Secret Manager, Docker Swarm secrets, or Kubernetes secrets) rather than plain environment variables where possible. If you're managing API keys for multiple apps or team members, a tool like SubToAPI's dashboard lets you issue and revoke scoped sub_live_... keys per application without touching container configs — useful once you have more than one service hitting Claude. See /pricing for how that maps to seats.
Step 5: docker-compose for local development
# docker-compose.yml
version: "3.9"
services:
claude-api-service:
build: .
ports:
- "3000:3000"
env_file:
- .env
restart: unless-stopped
Run it with:
docker compose up --build
This rebuilds the image on every change to the Dockerfile and starts the container with your local .env loaded automatically.
Step 6: Testing the container
Once running, verify both the health check and the actual Claude call:
curl http://localhost:3000/healthz
curl -X POST http://localhost:3000/ask \
-H "Content-Type: application/json" \
-d '{"prompt": "Say hello in five words"}'
If the second call fails with a 401, double-check that the environment variable is actually reaching the container — a common mistake is referencing process.env.CLAUDE_API_KEY in code but naming it ANTHROPIC_API_KEY in .env.
Production considerations
- Logging: send stdout/stderr to your log aggregator rather than writing to files inside the container — containers are ephemeral.
- Timeouts: Claude responses, especially with streaming or tool use, can take longer than a default reverse-proxy timeout. Set generous read timeouts on your load balancer.
- Scaling: this container is stateless, so horizontal scaling behind a load balancer works without extra coordination.
- Rate limits: if multiple container replicas share one API key, you'll hit per-key rate limits faster — consider centralizing calls through a proxy layer like SubToAPI, which gives each application its own key and usage dashboard without you managing rate-limit logic per container (/docs).
questions
Do I need a GPU or special base image to run Claude API calls in Docker? No. Claude runs on Anthropic's infrastructure, not inside your container — your container just makes HTTPS requests, so a standard slim or Alpine base image is sufficient.
How do I avoid leaking my API key when building the image? Never COPY a .env file or hardcode the key in the Dockerfile. Pass it as a runtime environment variable or secret, and add .env to .dockerignore and .gitignore.
Can I use the same Dockerfile for streaming responses? Yes — streaming doesn't change the container setup, only the HTTP response handling in your app code. Make sure your reverse proxy and load balancer timeouts are long enough to keep the connection open; see /docs/streaming for request-level details.