The full stack with docker-compose

You've wrapped one model in a dozen tools, one chapter at a time. In production they run together: the API, the worker, Redis, and the tracking server, all at once, talking to each other. Starting four services by hand in four terminals is painful and fragile. docker-compose defines the whole system in one file and launches it with one command.

Setup: Docker with Compose (bundled in Docker Desktop). Follow-along.

The whole system, declared

docker-compose.yml describes every service and how they connect:

services:
  redis:                                    # broker + cache
    image: redis:7-alpine
    ports: ["6379:6379"]

  api:                                      # the FastAPI model service
    build: .
    ports: ["8000:8000"]
    environment: [REDIS_URL=redis://redis:6379/0]
    depends_on: [redis]

  worker:                                   # Celery background worker
    build: .
    command: celery -A tasks.celery_app worker --loglevel=info
    environment: [REDIS_URL=redis://redis:6379/0]
    depends_on: [redis]

  mlflow:                                   # experiment tracking / registry UI
    image: ghcr.io/mlflow/mlflow:latest
    command: mlflow server --host 0.0.0.0 --port 5000 --backend-store-uri sqlite:////mlflow/mlflow.db
    ports: ["5000:5000"]
    volumes: [mlflow-data:/mlflow]

volumes:
  mlflow-data:

Four services, each from earlier chapters, now wired into one system. Read it top to bottom and you can see the whole architecture at a glance: which is itself a benefit.

One command to rule them all

cd code
docker compose up --build

Expected output:

[+] Running 5/5
 ✔ Network code_default      Created
 ✔ Container code-redis-1    Started
 ✔ Container code-mlflow-1   Started
 ✔ Container code-api-1      Started
 ✔ Container code-worker-1   Started
api-1     | INFO:     Uvicorn running on http://0.0.0.0:8000
worker-1  | celery@... ready.
mlflow-1  | Listening at: http://0.0.0.0:5000

The entire stack is live:

  • http://localhost:8000/docs: the prediction API
  • http://localhost:5000: the MLflow UI
  • the worker consuming background jobs from Redis
  • Redis brokering and caching

Stop it all with one command:

docker compose down

How the services find each other

Notice REDIS_URL=redis://redis:6379/0, the API reaches Redis by the service name redis, not an IP. Compose creates a private network where each service is reachable by its name. This is the key idea: services address each other by name, not address, so the same compose file works on any machine without editing IPs.

Don't be confused: depends_on waits for start, not ready. depends_on: [redis] makes Compose start Redis before the API container, but it doesn't wait for Redis to be accepting connections. A service that crashes because its dependency isn't ready yet needs a real healthcheck (like the one in our Dockerfile) plus retry-on-connect logic. "Started" ≠ "ready" is a classic compose gotcha.

docker-compose vs. Kubernetes

Don't be confused: compose vs. Kubernetes. docker-compose runs multiple containers on one machine, perfect for local development, CI, and small deployments. Kubernetes (K8s) runs containers across a cluster of machines with auto-scaling, self-healing, rolling updates, and load balancing, the standard for production at scale. The good news: a compose file maps conceptually onto K8s manifests, so what you learn here transfers. Start with compose; graduate to K8s when one machine isn't enough.

The complete lifecycle, assembled

Step back and look at what you've built across the book, the entire production loop from Chapter 0, now real:

  data ─► train ─► track (MLflow) ─► register ─► serve (FastAPI) ─► package (Docker)
       ─► scale (Celery+Redis) ─► version data (DVC) ─► orchestrate (Prefect)
       ─► optimize (ONNX) ─► demo (Streamlit) ─► monitor ─┐
       ▲                                                   │
       └──────────────── retrain on drift ◄────────────────┘

  GenAI stack:   vector DB ─► RAG service ─► LLM serving ─► LLM observability
  Engineering:   testing & CI/CD  ·  config, secrets & security
  … and run it all with one command (docker-compose)

Every box is a tool you can now use. Swap our tiny model for a real one and nothing about the tooling changes: that was the whole point of keeping the model trivial.

A production-readiness checklist

Before any model goes live, walk this list (each item maps to a chapter):

  • Experiments tracked and reproducible (MLflow, DVC)
  • Model versioned in a registry with a @production alias
  • Served behind a validated API with a /health check
  • Containerized; image in a registry; runs as non-root
  • Heavy work offloaded to a queue; hot paths cached
  • Retraining orchestrated and gated on quality
  • Inference optimized (ONNX/quantization) if latency matters
  • Monitoring for drift and operational metrics, with alerts
  • Tests passing in CI; deploys gated on green (Chapter 18)
  • Secrets out of code; API authenticated & rate-limited (Chapter 19)
  • For LLM features: cost/latency tracked, eval gate, grounded RAG (Chapter 17)
  • A rollback plan (move the alias back)

The takeaway

docker-compose declares your whole multi-service system, API, worker, Redis, MLflow, in one file and launches it with docker compose up; services find each other by name on a private network, and you graduate to Kubernetes when one machine isn't enough. You've now assembled the complete production loop: track, register, serve, package, scale, version, orchestrate, optimize, demo, monitor, and retrain. That's MLOps, and you can do it. Go ship something. 👉