Skip to main content

Docker Compose Basics

· 5 min read

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.

  1. 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.

  1. 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:

  1. 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.

  2. Default network: up automatically 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.

  3. Declarative management: on subsequent up runs, 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:

  • up without -d runs 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 always up -d.
  • The --tail=500 on logs starts 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.
  • stop only stops containers without removing them; rm removes stopped containers. To stop and remove containers and networks in one go, use docker-compose down.
  • After changing a Dockerfile, up -d alone won't rebuild the image — either build first and then up, or just run up -d --build.

Pitfalls and Notes

  1. Re-run up after editing the YAML: restart alone doesn't apply config changes — it just restarts the old containers. To land the new configuration you must run up -d again so compose recreates the containers that changed.

  2. 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.

  3. 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 -f to point at the file explicitly and avoid mounting the wrong path.

warning

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