Deploying the Gateway: Docker & Kubernetes

10 min read Module 9 of 10 Lesson 25 of 28

What you'll learn

  • Containerize the LiteLLM proxy with a config.yaml mounted as a volume and secrets injected as environment variables
  • Explain why the gateway's Redis dependency (used for usage-based routing since Lesson 9) needs its own deployment consideration
  • Write a Kubernetes Deployment and Service for the gateway with health checks wired to the proxy's own endpoints
  • Configure a horizontal pod autoscaler using a metric appropriate for gateway load, not just raw CPU
Building this at your company? For enterprise and company teams taking this to production: book a 30-minute session with our AI engineers for architecture guidance, code review, and a rollout plan for your use case.
Book a Team Session

Every lesson so far ran the LiteLLM proxy as a local process. This lesson deploys it for real: a proper Dockerfile, the Redis dependency load-balancing and rate limiting have depended on since Lessons 9 and 15, and a Kubernetes deployment sized around how a gateway actually behaves under load, which is different from a typical stateless web service.

Containerizing the Gateway

FROM python:3.12-slim

WORKDIR /app
RUN pip install --no-cache-dir 'litellm[proxy]' redis

# config.yaml is NOT baked into the image: it's mounted at runtime (via
# ConfigMap in Kubernetes, below) so a routing change is a config update,
# not an image rebuild and redeploy.
EXPOSE 4000

CMD ["litellm", "--config", "/etc/litellm/config.yaml", "--port", "4000", "--num_workers", "4"]
# docker-compose.yml: local multi-container setup, gateway + the Redis
# dependency used since Lesson 9 (usage-based routing) and Lesson 15
# (distributed rate limiting)
services:
  redis:
    image: redis:7-alpine
    ports: ["6379:6379"]

  gateway:
    build: .
    ports: ["4000:4000"]
    volumes:
      - ./config.yaml:/etc/litellm/config.yaml:ro
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
      - REDIS_HOST=redis
    depends_on: [redis]

Secrets flow in as environment variables from the deployment environment, not baked into the image, exactly the master-key/virtual-key separation from Module 3 taken to its logical conclusion: an image with a real provider key baked in is extractable by anyone who can pull it, which defeats the whole point of keeping raw provider credentials out of anything application code (or, here, a distributable artifact) can touch.

Kubernetes: Deployment, Service, Secrets

apiVersion: v1
kind: ConfigMap
metadata:
  name: litellm-config
data:
  config.yaml: |
    model_list:
      - model_name: production-chat
        litellm_params:
          model: anthropic/claude-sonnet-4-5
          api_key: os.environ/ANTHROPIC_API_KEY
    router_settings:
      redis_host: redis-service
      redis_port: 6379
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ai-gateway
spec:
  replicas: 3                          # multiple replicas: Redis (below) keeps their routing/rate-limit state consistent
  selector:
    matchLabels: {app: ai-gateway}
  template:
    metadata:
      labels: {app: ai-gateway}
    spec:
      containers:
        - name: ai-gateway
          image: ai-gateway:latest
          ports: [{containerPort: 4000}]
          envFrom:
            - secretRef: {name: ai-gateway-provider-keys}   # real provider keys, from a real secrets manager
          volumeMounts:
            - {name: config, mountPath: /etc/litellm}
          readinessProbe:
            httpGet: {path: /health/readiness, port: 4000}
            initialDelaySeconds: 5
          livenessProbe:
            httpGet: {path: /health/liveliness, port: 4000}
            initialDelaySeconds: 10
      volumes:
        - name: config
          configMap: {name: litellm-config}
---
apiVersion: v1
kind: Service
metadata:
  name: ai-gateway-service
spec:
  selector: {app: ai-gateway}
  ports: [{port: 80, targetPort: 4000}]

Wiring readinessProbe/livenessProbe to the proxy’s own health endpoints matters specifically for a gateway: a replica that can accept TCP connections but whose upstream provider connectivity is broken should be pulled from rotation by the readiness check, not left serving requests it can’t actually fulfill.

Sizing the Autoscaler for I/O-Bound Load

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: ai-gateway-hpa
spec:
  scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: ai-gateway}
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Pods
      pods:
        metric: {name: in_flight_requests}   # custom metric, not raw CPU
        target: {type: AverageValue, averageValue: "50"}
flowchart TD
    LB["Kubernetes Service"] --> P1["Gateway pod 1"]
    LB --> P2["Gateway pod 2"]
    LB --> P3["Gateway pod 3"]
    P1 --> Redis[("Shared Redis\nrouting + rate-limit state")]
    P2 --> Redis
    P3 --> Redis
    HPA["HPA: in_flight_requests"] -.->|scales| P1
    HPA -.->|scales| P2
    HPA -.->|scales| P3

    style LB fill:#4338CA,stroke:#4338CA,color:#ffffff,rx:8px,ry:8px
    style Redis fill:#F7F8FC,stroke:#0D9488,color:#0F172A,rx:8px,ry:8px
    style HPA fill:#EEF0F7,stroke:#6366F1,color:#0F172A,rx:8px,ry:8px

Raw CPU is a weak autoscaling signal here because a gateway is fundamentally I/O-bound, most of the time handling a request is spent waiting on a provider’s network response, not consuming CPU, so CPU usage can stay deceptively low right up until the gateway is genuinely saturated on concurrency. A metric that reflects real load, in-flight request count (shown above) or p99 latency, scales the deployment based on what’s actually happening rather than a proxy signal that lags behind it. With the gateway containerized and deployed, Lesson 26 extends this to multiple regions and the failure scenarios a single-region deployment, however well autoscaled, still can’t survive.

Knowledge Check

3 questions to test your understanding

1 Why should the gateway's config.yaml and secrets (provider API keys) be handled differently when containerizing, config as a mounted file or ConfigMap, secrets as environment variables from a secrets manager, rather than baking both directly into the Docker image?

2 Why does the gateway's Redis dependency, used for usage-based routing (Lesson 9) and distributed rate limiting (Lesson 15), need explicit deployment consideration rather than just running as another container with no further thought?

3 Why is raw CPU usage often a poor metric to base a gateway's horizontal pod autoscaler on, compared to something like in-flight request count or request latency?

Go further with expert guidance

Ready to build production AI?
Talk to our R&D team.

These courses give you the foundation. Our embedded AI teams take you from prototype to production in 30–90 days, with your team, your codebase, your goals. Book a free strategy call to see how we can accelerate your AI initiative.

30 minutes · No obligation · Expert AI engineers, not sales reps

AI Architecture Review

Audit your current stack and identify high-impact improvements

Project Review

Get expert feedback on your AI implementation and codebase

Team Mentoring

Upskill your engineers with hands-on AI coaching sessions

AI Strategy

Define your AI roadmap, prioritization, and implementation plan