You clone a repository on a Monday morning, open the README, and find a familiar list: install PostgreSQL, create a database, seed it, start Redis, set six environment variables, then run the app. Half an hour later you are still fixing a version mismatch instead of writing code. Docker Compose exists to turn that list into one command.
What Docker Compose is actually for
Docker runs one container: one isolated process with its own filesystem, network and environment. That is enough for a single service, but most local projects are not single services. A typical web application needs the app itself, a database, and often a cache, a queue or a mail catcher for testing outgoing email.
Compose is the layer on top that describes those containers together. You write one YAML file — compose.yaml or docker-compose.yml, both are recognised — and Compose creates every container, wires them onto the same network, and starts them in a sensible order. The file lives in your repository, so a new developer gets the same environment you have, on macOS, Linux or Windows.
It is not a production orchestration tool. Compose runs on one machine. That is exactly what you want for local development, and knowing the boundary saves you from trying to scale it later.
Writing your first compose file
Start small. A file with two services is enough to learn the shape of it. The top-level key is services, and under it you name each container yourself.
A database service might use the image key with postgres:16-alpine, publish port 5432 with ports, and set a password through environment. Your application service usually uses build instead of image, pointing at the folder containing its Dockerfile.
The keys you will use most
- image — pull a ready-made container from a registry.
- build — build from your own Dockerfile, usually with a context path.
- ports — map a container port to your host so you can reach it in a browser.
- environment — pass configuration in as variables.
- volumes — persist data, or mount your source code into the container.
- depends_on — express a start-up relationship between services.
Indentation matters in YAML, and tabs will break the file. Use two spaces, keep service names lowercase, and run docker compose config whenever something behaves strangely — it validates the file and prints what Compose actually understood.
The commands you will use every day
Modern Docker ships Compose as a plugin, so the command is docker compose with a space. The older hyphenated docker-compose still appears in tutorials; if it fails, try the spaced version.
- docker compose up — build if needed, create and start everything, and stream the logs to your terminal.
- docker compose up -d — the same, in the background, so you get your prompt back.
- docker compose logs -f api — follow one service's output without the noise from the rest.
- docker compose exec db psql -U postgres — open a client or shell inside a running container.
- docker compose down — stop and remove the containers, keeping named volumes intact.
- docker compose down -v — the same, but delete the volumes too. Useful when you want a clean database, dangerous when you do not.
If a build seems stuck on old code, docker compose build --no-cache forces a fresh one. It costs a few minutes and settles most "but I changed that file" arguments.
Volumes, bind mounts and where your data lives
Containers are disposable. Delete one and anything written inside it disappears. That is fine for your application code, which is baked into an image, but not for database files.
Named volumes
A named volume is storage Docker manages for you. You declare it in the volumes key of a service and Docker keeps the contents between restarts. Databases belong here. docker compose down leaves them alone; down -v removes them.
Bind mounts
A bind mount maps a folder from your machine into the container, which is how live reloading usually works: edit a file on your laptop, and the process inside the container sees the change straight away. On macOS and Windows this is slower than on Linux, which is why some teams mount only the source directory and leave dependencies inside the image.
Environment variables, ports and start-up order
Hard-coded credentials in a committed file are a bad habit even locally. Put them in a .env file next to the compose file, reference them in environment, and add .env to .gitignore. Keep a .env.example in the repository so the next person knows which values to supply.
depends_on is not a guarantee
depends_on controls the order containers are started, not that the service inside is ready. PostgreSQL can take several seconds to accept connections while its container already counts as running. Add a healthcheck to the database service and depend on it being healthy, or make your application retry its connection on start-up. The second approach is less configuration and often more reliable.
Mistakes that cost an afternoon
- Publishing the same port twice — two services both asking for 5432 on your host will clash. Change the host side, such as 5433:5432.
- Using localhost inside a container to reach a sibling service. From inside the network, use the service name, for example db.
- Assuming down wipes the database. It does not, unless you pass -v.
- Committing .env. Secrets reach a remote repository faster than anyone expects.
- Growing one file until it starts ten services nobody can explain. Split with multiple -f files when it earns its keep, not before.
Keep it small, keep it committed
The value of Compose is not clever configuration. It is that a colleague can clone your repository, copy .env.example to .env, run docker compose up, and be working within minutes. Aim for that, and add to the file only when a real need appears.
Two habits are worth building early. Run docker compose config before you commit, so broken YAML never reaches anyone else. And put the three commands a newcomer needs at the top of the README: one to start the stack, one to run the tests, one to reset the database. Your future self, three months from now with no memory of the setup, will be the main beneficiary.
Photo: jamesmarkosborne / Pixabay





