← Blog

Claude API Docker Container Setup Tutorial

2026-10-19 · 5 min read · SubToAPI Team

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

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:

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

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.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →