Docker Compose is a tool for defining and running multi-container applications from a single declarative YAML file, usually named compose.yaml. In the field of containers within cloud infrastructure, it describes an application as a set of services, networks and volumes and lets one command create, start, stop or remove all of them together on a single Docker host.
Compose sits one level above individual containers. A Dockerfile describes how to build one container image, and the Docker engine runs one container at a time; Compose describes how several containers cooperate, which images they use, how they reach each other and which data they keep. It is the standard way to reproduce a complete application stack on a developer workstation or a small server.
How Docker Compose turns a file into running containers
Docker Compose works by reading the Compose file, resolving it into a full model and asking the Docker engine to make the real state match that model. The current implementation is a plugin of the Docker command line invoked as docker compose, written in Go; the older standalone docker-compose program written in Python is deprecated and no longer maintained.
When a user runs docker compose up, the tool merges every referenced file, interpolates variables from the shell and from a .env file, and builds or pulls the required images. It then creates a project-scoped default network, named volumes and finally the containers, each labeled with the project name. Because the labels record ownership, a later command can find, recreate or remove exactly the resources of that project.
Compose is declarative and convergent. Running up again after editing the file recreates only the services whose configuration changed and leaves the others running. This behavior makes the file the single source of truth for the stack, which can be versioned alongside the application code.
The following table lists the commands used most often in daily work and what each one changes.
| Command | Effect | Typical use |
|---|---|---|
docker compose up -d | Creates and starts all services in the background | Start the whole stack |
docker compose down | Stops and removes containers and the default network | Clean shutdown; add -v to delete volumes |
docker compose build | Builds images for services with a build section | Rebuild after code changes |
docker compose logs -f | Streams combined output of services | Debug startup problems |
docker compose exec | Runs a command in a running service container | Open a shell or run migrations |
docker compose config | Prints the fully resolved model | Verify merges and variables |
Structure of the Compose file
A Compose file is organized into top-level elements, defined by the open Compose Specification. The legacy version key is now obsolete and ignored; current tooling infers the schema from the content itself.
| Element | Purpose | Example content |
|---|---|---|
| services | Defines each container and its runtime settings | Image, ports, environment, healthcheck |
| networks | Declares virtual networks for service communication | A separate back-end network |
| volumes | Declares named persistent storage | Database data directory |
| configs | Provides non-sensitive configuration files | A web server configuration |
| secrets | Provides sensitive values as files | Database password |
Inside a service, the most important keys are image or build, ports, environment, volumes, depends_on, healthcheck, restart and profiles. Services on the same network reach each other by service name through built-in DNS, so a web service connects to db:5432 rather than to a fixed IP address.
A frequent source of confusion is depends_on. By default it only controls start order, not readiness. To wait until a database actually accepts connections, the dependency needs condition: service_healthy together with a healthcheck on the database service.
Docker Compose compared with related tools
Docker Compose occupies the space between running single containers by hand and operating a cluster orchestrator. The comparison below shows where its scope ends.
| Tool | Scope | Strength | Limitation |
|---|---|---|---|
| docker run | One container | Simple and immediate | Long commands, no stack definition |
| Docker Compose | One host, many containers | Readable, reproducible stack file | No multi-node scheduling or failover |
| Docker Swarm | Small cluster | Accepts a similar file format | Smaller ecosystem than Kubernetes |
| Kubernetes | Large clusters | Self-healing, autoscaling, rich APIs | Much higher operational complexity |
Compose files do not run on Kubernetes directly. Conversion tools can translate them into Kubernetes manifests, but the result usually needs manual work on probes, resource limits and ingress. Teams therefore treat Compose as a development and small-deployment tool and maintain separate manifests or Helm charts for production clusters.
Practical use and configuration choices
Compose is strongest wherever a repeatable multi-service environment matters more than high availability. Several features help adapt one definition to different situations without duplicating it.
- Override files:
compose.override.yamlis merged automatically, so development settings such as source-code bind mounts stay out of the base file. - Multiple files with -f: a production file can be layered on the base file explicitly.
- Profiles: optional services such as an admin tool start only when their profile is activated.
- Variable interpolation: values such as image tags come from the environment or a
.envfile. - Watch mode:
docker compose watchsyncs or rebuilds when source files change.
The decision table summarizes when Compose is a sound choice and when a different tool fits better.
| Situation | Recommended choice | Reason |
|---|---|---|
| Local development of a multi-service app | Docker Compose | One command reproduces the full stack |
| Integration tests in a CI pipeline | Docker Compose | Disposable, isolated dependencies per run |
| Small internal tool on one server | Docker Compose with restart policies | Adequate when brief downtime is acceptable |
| Production service needing failover | Kubernetes or a managed platform | Compose cannot move containers between hosts |
Common mistakes and pitfalls
Most problems with Docker Compose come from assumptions about readiness, data and secrets rather than from the tool itself.
| Symptom | Cause | Fix |
|---|---|---|
| Application fails to reach the database at startup | depends_on without a health condition | Add a healthcheck and service_healthy |
| Database data disappears | down -v removed named volumes | Omit -v and back up volumes |
| Passwords visible in the repository | Secrets written into environment | Use secrets or an untracked .env file |
| Port conflict on the host | Two projects publish the same host port | Change the host port or drop the mapping |
| Old code keeps running | Image not rebuilt after changes | Run up --build or use watch mode |
Another pitfall is publishing every port to the host. Services that only talk to each other need no ports entry at all; leaving internal services unpublished reduces the attack surface and avoids conflicts.
A non-technical view
Modern applications are rarely one program. A typical web shop needs a website, a database and perhaps a cache, each running in its own separate container. Starting them one by one, in the right order and with the right connections, is tedious and easy to get wrong.
Docker Compose works like a recipe card for the whole meal. The card lists every dish, what goes into it and how the dishes are served together. Anyone with the card and a kitchen can reproduce the same meal with one instruction. The key idea is one file that starts the entire application at once.
A concrete example
A team of four developers builds an online booking system made of a Python web service, a PostgreSQL database, a Redis cache and a background worker. Before using Compose, each new developer spent most of a day on setup, installing matching versions of every component, and test results differed between laptops.
The team writes one `compose.yaml` with four services, a named volume for the database and a healthcheck that runs pg_isready. The web service depends on the database with condition: service_healthy, and a development override mounts the source code for live reloading. The same file, without the override, starts disposable dependencies for integration tests in the CI pipeline.
The outcome is that a new developer runs the full stack in minutes with docker compose up, and tests behave the same locally and in CI. When the product later moves to a cluster, the team writes Kubernetes manifests for production but keeps Compose for development.