5 · docker-compose for serving
Long `docker run` commands with a dozen flags are error-prone and unrepeatable. docker-compose writes the whole multi-container setup — ports, env vars, volumes — into one declarative YAML file you start with a single command.
docker-compose declares an entire multi-container application — services, their port mappings, environment variables, and volume mounts — in ONE `docker-compose.yml`, so `docker compose up` reproducibly starts the whole stack instead of a fragile, hand-typed `docker run` with a dozen flags.
Without this:
Without compose, every teammate runs the model with a slightly different `docker run` command, ports and env vars get forgotten, and wiring a second container (a tracking server, a database) becomes manual networking pain.
A real serving setup needs more than docker run image. You map ports, set environment variables (the model path, log level), mount a volume so the container can read the model file from disk, and often run a second container alongside (an MLflow tracking server, a database, a cache). Expressing all that as docker run flags gets unwieldy fast — and the next person has to retype the exact same long incantation, or it won't work the same.
docker-compose solves this. Instead of remembering flags, you declare the whole setup in a docker-compose.yml file. Then docker compose up reads the file and starts everything, and docker compose down tears it all back down. The configuration lives in version control, so it's reproducible and reviewable.
The three things compose configures that matter most for serving:
ports—"8000:8000"maps a host port to a container port, exactly like-pondocker run. This is how requests from outside reach the model API inside the container.environment— sets environment variables inside the container:MODEL_PATH=/models/model.joblib,LOG_LEVEL=info. Your app reads these instead of hard-coding values, so the same image behaves differently per environment (dev vs prod).volumes—"./models:/models"mounts a host folder into the container. This is how the model artifact gets in: rather than baking a large.joblibinto the image (which would force an image rebuild every time you retrain), you mount themodels/folder, and the container reads the latest model from disk at startup. Swap the file, restart the container, new model — no rebuild.
What compose adds over docker run. It's declarative (the setup is a file, not a typed command), reproducible (everyone runs the identical stack), version-controlled, and — its signature strength — it orchestrates multiple containers on a shared network with one command. Need to run the API and an MLflow tracking server? Two services in the same YAML, and compose wires them together automatically.
A `docker-compose.yml` declaring a model API service (ports, env vars, a mounted model volume) plus an optional MLflow tracking server, all started with one `docker compose up`.
What does docker-compose primarily add over a bare `docker run` command?
- docker-compose declares a whole multi-container stack in one `docker-compose.yml`; `docker compose up` starts it reproducibly, `docker compose down` tears it down.
- For serving, the key fields are `ports` (host:container), `environment` (model path, log level), and `volumes` (mount the model folder so retraining needs no image rebuild).
- compose's signature strength is orchestrating multiple containers on a shared network — the API can reach a co-located MLflow server by its service name, no manual wiring.
Local ML serving stacks (API + tracking server + database) are almost always brought up with docker-compose — it's the standard way to run and share a multi-service ML setup before it goes to the cloud.
If you remove it: You'd juggle several long `docker run` commands by hand, manually wire container networking, and watch each teammate's setup drift — exactly the irreproducibility this track fights.