Docker Compose Basics
Once Docker is installed, we end up running lots of containerized services — and orchestrating them all becomes a problem: network grouping, startup order, volume mounts. That's why the Docker team built docker-compose, a component that makes container orchestration easy.
Why Orchestration
A single container is easy to start with docker run, but real applications rarely consist of just one: a web service usually comes with a database, a cache, and a message queue. These containers depend on each other, need to reach each other on the same network, and each mounts its own volumes. If everything relies on hand-typed docker run commands, nobody remembers the arguments once they get long — and redeploying on a new machine is torture.
docker-compose solves exactly this: it captures every container's startup parameters in a single YAML file that lives under version control, so anyone with the file can reproduce the whole environment with one command.
Installation
The open-source project lives at:
https://github.com/docker/compose
Download the binary matching your system architecture from the project's Releases page. Installation takes two steps.
- Download the file and upload it to /usr/local/bin on the server.
We put it in /usr/local/bin because that directory is on the PATH by default, so once it's there you can run docker-compose from anywhere.
- Make the file executable
# Grant execute permission, otherwise the shell reports Permission denied
sudo chmod +x /usr/local/bin/docker-compose
After installing, run docker-compose version to verify — if it prints version info, you're good.
How It Works
docker-compose is just a binary that runs directly on Linux. It lets you configure every container your application needs in a YAML file, then create and start all the services from that configuration with a single command: docker-compose up -d.
It's not a separate container engine — under the hood it does exactly what manual docker run commands do: it parses the YAML and calls Docker's API to create networks, volumes, and containers. A few key mechanisms:
-
Project: compose defaults the project name to the directory containing the YAML file. Containers, networks, and volumes created within a project carry that prefix, keeping projects isolated from each other.
-
Default network:
upautomatically creates a bridge network and puts every service defined in the file on it. Containers on the same network can reach each other using service names as hostnames — no need to care about container IPs. -
Declarative management: on subsequent
upruns, compose diffs the YAML against the current container state and only recreates services that changed, leaving the rest untouched.
A typical docker-compose.yml looks roughly like this:
version: "3"
services:
web:
image: nginx # image to use
ports:
- "80:80" # host port:container port
volumes:
- ./html:/usr/share/nginx/html # volume mount
depends_on:
- app # startup order: app first, then web
app:
build: ./app # can also build on the spot from a Dockerfile
restart: always # restart automatically if the container exits abnormally
Common Commands
All the commands below must be run from the directory containing docker-compose.yml, or with -f pointing to the file.
Common commands:
docker-compose up [start containers; add -d to run in the background]
docker-compose stop [stop containers]
docker-compose ps [list all orchestrated services]
docker-compose logs -f --tail=500 <container name> [follow a container's logs live]
docker-compose restart [restart containers]
docker-compose rm [remove containers]
docker-compose build [build images]
A few extra notes:
upwithout-druns in the foreground and streams every container's logs to your terminal — handy for debugging, and Ctrl+C stops everything. For day-to-day deployment it's almost alwaysup -d.- The
--tail=500onlogsstarts following from the last 500 lines only; without it the full log history floods out, and with a large log volume the terminal hangs for a long while. stoponly stops containers without removing them;rmremoves stopped containers. To stop and remove containers and networks in one go, usedocker-compose down.- After changing a Dockerfile,
up -dalone won't rebuild the image — eitherbuildfirst and thenup, or just runup -d --build.
Pitfalls and Notes
-
Re-run up after editing the YAML:
restartalone doesn't apply config changes — it just restarts the old containers. To land the new configuration you must runup -dagain so compose recreates the containers that changed. -
depends_on only controls startup order: it guarantees containers start in order, but not that the dependencies are actually ready. For example, the database container may be up while the process isn't yet accepting connections — the app will error out if it connects at that moment, so the app side needs retry logic.
-
Watch your working directory with relative volume paths: relative paths in the YAML are resolved relative to the YAML file's location. When running from a different directory, use
-fto point at the file explicitly and avoid mounting the wrong path.
docker-compose down with the -v flag deletes the volumes along with everything else. Before running it against stateful services (databases, etc.), make absolutely sure the data in those volumes is disposable.
Wrapping Up
docker-compose is essentially a pile of docker run arguments frozen into a version-controllable YAML file: one up -d brings up the whole stack, and logs, ps, and restart cover everyday operations. On a single machine it's more than comfortable enough — once your services grow to the point of needing multi-machine scheduling, that's the time to look at orchestration systems like Kubernetes.
COMMENTS