Area 07 // DeploymentLesson 110 / 14013 min read
Docker Compose
Docker Compose describes all the containers an app needs (the API, a database, a cache, a local model server) in one compose.yaml file, so a single command starts the whole stack with the right ports, settings, volumes and health checks, on a laptop, in CI or on a small server.
Think of it like this
Analogy
A band's stage plan: one sheet that says where the drums, guitars, microphones and monitors go, how they're wired together, and in what order to switch them on. The crew can set up the whole show in any venue from that one sheet. compose.yaml is the stage plan for your app's containers.
The problem it solves
A real AI app is rarely one container. The API needs Postgres, Redis, maybe a vector database and a local model server for development. Starting each with long docker run commands, remembering ports, networks, environment variables and the right start-up order, is fragile, and every teammate does it slightly differently. You want the whole stack described once and started with one command.
How it works, step by step
- Write
compose.yamlwith aservices:section; each service is a container built from your Dockerfile (build:) or pulled from a registry (image:). - Ports:
"8000:8000"maps a port on your machine to the container. - Environment: settings per service, with
${VAR:-default}reading from your shell or a git-ignored.envfile (environments and config). - Networking: services reach each other by name (
http://ollama:11434,postgres:5432) on a private network. - Volumes keep data across restarts: databases, Redis snapshots, downloaded models (Ollama).
- Health checks and order:
healthcheck:plusdepends_on: { db: { condition: service_healthy } };docker compose up --waitwaits until every service is healthy (health checks). - Profiles make optional services opt-in (
--profile local-llm). - Commands:
docker compose up -d --wait,ps,logs -f app,exec app sh,down(add-vto delete volumes). - Where it fits: local development, CI smoke tests (as in the capstone) and small single-server deployments; clusters use Kubernetes or managed platforms.
See it in code
- Needs
- Docker with the Compose plugin. The file is examples/rag-app/compose.yaml in the course repository.
The capstone's Compose file (the app, plus an optional local model server), then the real smoke test from the course's CI run.
services:
app:
build:
context: .
args: { APP_VERSION: "${APP_VERSION:-compose}" }
ports: ["8000:8000"]
environment:
LLM_PROVIDER: ${LLM_PROVIDER:-mock}
LLM_MODEL: ${LLM_MODEL:-llama3.2}
OLLAMA_HOST: http://ollama:11434 # reach the other service by name
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} # from your shell or a git-ignored .env
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/ready')"]
interval: 5s
retries: 5
restart: unless-stopped
ollama: # only with: --profile local-llm
image: ollama/ollama
profiles: ["local-llm"]
volumes: ["ollama-models:/root/.ollama"] # downloaded models survive restarts
ports: ["11434:11434"]
volumes:
ollama-models:$ APP_VERSION=31fab5b docker compose up -d --wait
Image rag-app-app Built
Network rag-app_default Created
Container rag-app-app-1 Started
Container rag-app-app-1 Waiting
Container rag-app-app-1 Healthy
$ docker compose ps
NAME IMAGE SERVICE STATUS PORTS
rag-app-app-1 rag-app-app app Up 5 seconds (healthy) 0.0.0.0:8000->8000/tcp
$ curl -s localhost:8000/ready
{"ready":true,"checks":{"index_loaded":true,"provider_configured":true},"version":"31fab5b"}
$ curl -s -X POST localhost:8000/ask -d '{"question": "How many days of annual leave do I get?"}'
{"answer":"Full-time employees get 25 days of annual leave per year, plus public holidays. [1]", ...}
$ docker compose down
Container rag-app-app-1 Removed
Network rag-app_default RemovedThis ran inside the capstone's GitHub Actions build job. --wait returned only after the container's health check (which calls /ready) passed, so the curl calls never raced the start-up. The Ollama service didn't start, because its profile wasn't enabled.
A real example
Example
A one-command dev environment
A team's onboarding doc for their RAG product was two pages of install steps for Postgres with pgvector, Redis, the API and a local model. New joiners took a day to get running. They replaced it with a compose.yaml: Postgres (with a volume and a health check), Redis, the API built from the Dockerfile with depends_on conditions, and Ollama behind a local-llm profile so laptops without much memory could skip it. Onboarding became cp .env.example .env && docker compose up --wait, and the same file runs the integration tests in CI.
The easy way
Start here
Put your app and each dependency it needs (database, cache) as services in one compose.yaml, add a health check to each, and use docker compose up -d --wait locally and in CI. Keep secrets in a git-ignored .env.
When you grow: the production setup
Teams keep a base compose.yaml plus override files per purpose (compose.dev.yaml with hot reload and mounted source, compose.ci.yaml for tests), pin image versions, add resource limits, use profiles for heavy optional services, run integration and smoke tests against the Compose stack in CI, and move to Kubernetes or a managed platform when they need multiple machines.
Trade-offs
What you gain
- Whole stack described in one reviewed file.
- One command to start, stop and reset.
- Same setup for laptops and CI.
What it costs you
- Single machine only; no built-in scaling across servers.
- Limited self-healing compared with an orchestrator.
- Heavy services (local models) can exhaust laptop memory.
When not to use it
- Production systems that need high availability across several machines: use Kubernetes or a managed platform.
Common mistakes
- Secrets written directly in
compose.yaml. - No health checks, so tests start before the app is ready.
- Using
localhostto reach another service instead of its service name. - Forgetting volumes, then losing the database on
down. - Running
down -vand deleting data by accident.
Interview questions
Try answering out loud first, then open the model answer.
What is Docker Compose used for?
Model answer
Defining and running multi-container applications from one YAML file: services with images or builds, ports, environment, volumes, networks, health checks and dependencies. It's ideal for local development environments, integration and smoke tests in CI, and simple single-host deployments.
How do containers in the same Compose project talk to each other?
Model answer
Compose creates a project network and registers each service's name in DNS, so a container reaches another at http://service-name:port. Ports only need publishing to the host for access from outside the project.
Docker Compose vs Kubernetes?
Model answer
Compose runs containers on one machine with simple declarative config: great for development, CI and small deployments. Kubernetes schedules containers across a cluster with self-healing, rolling updates, autoscaling, service discovery and richer networking and storage: suited to production at scale, at the cost of much more complexity.
Quiz
Pick an answer to see whether it’s right and why.
0 of 5 answered
Key terms
- Docker Compose
- A tool that starts several containers together from one compose.yaml file, with their ports, settings, volumes, health checks and network.
- Container
- A lightweight, isolated package of an app and everything it needs to run, sharing the host's kernel. Docker is the most common tool. Also used as a sandbox.
- Volume
- Storage managed by Docker that lives outside a container, so data such as databases or downloaded models survives when the container is replaced.
- Health check
- A small endpoint (like /health or /ready) that the platform calls to decide whether an instance is alive and should get traffic.
- Service (Compose)
- One named container definition in a compose.yaml file, such as app, db or ollama. Other services reach it by that name.
Before and after
Read first
Progress is saved in this browser only.