Title: Lecture 4 — From Single Containers to Orchestrated Stacks
Course: AIINFRA 101: Applied Infrastructure & Containerization > Section: Week 04 — Docker Compose & Multi-Container AI Applications

📖 Lecture — From Single Containers to Orchestrated Stacks

Last week you learned to package a single AI service into a Docker container and run it with docker run. Real AI applications, though, are rarely just one container. A typical inference-serving system might need an API layer, a cache to avoid recomputing results, a database to track requests, and maybe a separate container for batch model training. Starting each of these by hand with a long string of docker run flags — remembering the right network, the right volume mounts, the right environment variables, every single time — does not scale. This is the problem Docker Compose solves. Compose as a declarative blueprint. A Compose file (docker-compose.yaml or compose.yaml) describes your entire application as a single document instead of a sequence of commands. It has three main building blocks:

Concept | What it declares | Real-world analogy
services | Each containerized component of your app (e.g., app, redis, trainer) | The individual departments in a company
networks | The communication paths that let services talk to each other | The internal phone system connecting departments
volumes | Persistent storage that survives container restarts | The shared filing cabinet everyone can access

Once this file exists, docker compose up reads it and builds your entire multi-container application in one command. docker compose down tears it all back down. You no longer retype ports, networks, and mounts by hand — you edit one YAML file and re-run the same command. Service discovery is automatic. One of the most useful things Compose does for you is networking. By default, Compose creates a private network for your whole application, and every service on that network can reach every other service using its service name as a hostname. If your docker-compose.yaml defines a service called database, your app service can connect to it at database:5432 — no IP addresses, no manual DNS configuration, no extra setup. This is what makes wiring together a FastAPI service and a Redis cache so simple: the FastAPI container just points its Redis client at host redis, and Compose's internal DNS resolves it to the right container every time, even if the container is destroyed and recreated with a new IP address. Giving a service a GPU. For AI workloads, some services need GPU acceleration — for example, a model-serving container running inference. Compose exposes this through the deploy.resources.reservations.devices field in a service definition. Setting driver: nvidia and capabilities: [gpu] tells Compose to reserve a GPU (or GPUs) for that specific service, alongside NVIDIA Container Toolkit on the host. This means a single Compose file can mix GPU-backed services (like a model server) with plain CPU services (like a cache or a lightweight API gateway) — you don't need to give every container GPU access, only the ones that need it. Ordering is not the same as readiness — the most common mistake. This is worth slowing down for, because it trips up almost everyone the first time. The depends_on field controls the order in which Compose starts your containers — if app depends on database, Compose will start the database container before it starts app. But "started" and "ready to accept connections" are two very different things. A database container can report as "started" the instant its process launches, while the actual database engine inside might take several more seconds to finish initializing before it can accept a connection. If your app tries to connect during that gap, you get a connection-refused error — a classic race condition. The fix is to add a healthcheck to the dependency service (a command Compose runs periodically to confirm the service is truly ready, such as pinging the database), and then change depends_on to the long form with condition: service_healthy. Now Compose will wait until the healthcheck actually passes before starting the dependent service — true readiness, not just a launched process. "It's running" is not "it's working." A related trap: when docker compose up finishes and your terminal shows all containers as Up, that only means the containers started successfully — it says nothing about whether the application logic inside them is actually functional. Compose has no way to know that your FastAPI route works or that your model finished loading into memory unless you tell it how to check. Treat a clean docker compose up as "the containers launched," and use healthchecks (or manual curl tests) as your actual signal that the application is ready to serve traffic. As of Compose v5.3.0 (current in 2026), the docker compose CLI plugin ships bundled with Docker Desktop and Docker Engine, so if you have Docker installed from this week's lab onward, you already have everything you need — no separate installation step.