Docker Compose Explained: compose.yaml, up, down and .env

Docker Compose lets you describe several containers, their ports, volumes, environment variables and networks in one YAML file called compose.yaml, then start them all with docker compose up and remove them with docker compose down. It replaces a list of long docker run commands with a file you can commit next to your code.

docker compose up -d     # create and start everything in compose.yaml, in the background
docker compose ps        # list the project's containers
docker compose logs -f   # follow output from all services
docker compose down      # stop and remove containers and networks

What problem Docker Compose solves

A typical web app needs at least two containers: the app and a database. Started by hand, that means creating a network, a volume for the database, then two docker run commands with a dozen flags between them, typed in the right order every time (the individual commands are in the Docker commands cheat sheet). Compose moves all of it into a declarative file. Docker's documentation describes the model in three parts:

  • Services: each one is a container (or several identical copies) run from one image with one configuration.
  • Networks: how services reach each other.
  • Volumes: persistent data that outlives individual containers.

Everything in one file belongs to a project. By default the project name is the name of the folder containing the file, and Compose prefixes the containers, network and volumes it creates with that name.

A complete compose.yaml example

This file runs a web app built from the local Dockerfile plus a PostgreSQL 16 database:

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: app
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      retries: 5
volumes:
  db-data:

Line by line:

  • build: . builds the web image from the Dockerfile in the same folder. Use image: instead when the image already exists on a registry, as db does.
  • ports: "8000:8000" publishes container port 8000 on host port 8000 (host first, container second).
  • ${DB_PASSWORD} is filled in from your shell or a .env file (next section).
  • depends_on with condition: service_healthy makes Compose wait until the database passes its healthcheck before starting web.
  • db-data:/var/lib/postgresql/data stores the database files in a named volume, declared at the bottom, so they survive the container being recreated.

The file name matters. Compose looks for compose.yaml (preferred) or compose.yml, and still accepts the older docker-compose.yaml and docker-compose.yml. If you have a file with another name, pass it with -f.

How services talk to each other

Notice the database address in DATABASE_URL: db:5432. Compose creates a network called <project>_default, attaches every service to it, and registers each service name in Docker's internal DNS. So the web container reaches the database at hostname db.

Two port rules from the Compose networking docs are worth memorising:

  • Service-to-service traffic uses the container port (5432 here), not any published host port.
  • The host port in ports: only matters for connections from outside, such as your browser. The db service above has no ports: entry at all, so the database is reachable by web but not exposed on your machine.

A common mistake is pointing an app at localhost:5432 inside a container. Inside a container, localhost is that container itself, so use the service name.

Environment variables and the .env file

Compose reads a file named .env from the project directory automatically and uses it to fill in ${VARIABLE} references in compose.yaml. Create one next to the compose file:

DB_PASSWORD=change-me-to-something-long

Generate a real value with a password generator such as larely's password generator, and add .env to .gitignore so the secret isn't committed. Values set in your shell override the .env file.

If the variable isn't set anywhere, Compose doesn't stop; it substitutes an empty string and warns you. Real output from docker compose config with no .env:

level=warning msg="The \"DB_PASSWORD\" variable is not set. Defaulting to a blank string."

To make a missing value a hard error instead, write ${DB_PASSWORD:?DB_PASSWORD must be set}; Compose then refuses to start with required variable DB_PASSWORD is missing a value: DB_PASSWORD must be set. To supply a default, write ${DB_PASSWORD:-devpassword}.

Check the file before you run it: docker compose config

docker compose config            # print the fully resolved file
docker compose config --services # just the service names

config parses the file, fills in variables and prints the result in canonical form, so you can see exactly what Compose will do. It works even when the Docker engine isn't running. For the example above it shows the generated names, which is where the project prefix becomes visible (real output, run in a folder named myapp; the end of the file):

networks:
  default:
    name: myapp_default
volumes:
  db-data:
    name: myapp_db-data

A YAML indentation mistake shows up here as an error such as yaml: line 1: did not find expected key before anything is created.

The Compose commands you'll use

CommandWhat it does
docker compose up -dBuild missing images, create or recreate containers, start them in the background
docker compose up -d --buildRebuild images first (after changing your code or Dockerfile)
docker compose psList the project's containers and their state
docker compose logs -f webFollow one service's output (omit the name for all)
docker compose exec db psql -U appRun a command in a running service container
docker compose run --rm web pytestStart a one-off container for a service
docker compose stop / startStop containers without removing them, then start them again
docker compose downStop and remove containers and the project's networks
docker compose down -vSame, plus delete named and anonymous volumes (your data)
docker compose pullDownload newer versions of the services' images

Run them from the folder containing compose.yaml; Compose also searches parent folders for the file. When you edit compose.yaml and run up -d again, Compose only recreates the containers whose configuration or image changed, and keeps mounted volumes.

depends_on doesn't mean "ready"

Plain depends_on: [db] only controls start order. Docker's docs are explicit that Compose waits until a dependency's container is running, not until the program inside is ready to accept connections. A database container is "running" a few seconds before PostgreSQL accepts logins, which is why apps started this way often crash on the first attempt. The fix is the pattern in the example: give the dependency a healthcheck and use condition: service_healthy. The other conditions are service_started (the default) and service_completed_successfully, for one-shot jobs such as migrations.

Compose v1, v2 and the version key

Older tutorials use docker-compose with a hyphen. That is Compose v1, which Docker's migration guide says is no longer maintained. The current version is invoked as docker compose, a plugin of the Docker CLI. Old tutorials also start files with version: "3.8". Current Compose ignores it and warns (real output from Compose v2.27, path shortened):

level=warning msg="docker-compose.yml: `version` is obsolete"

Delete the line. If you need a fixed project name instead of the folder name, use the top-level name: key or docker compose -p myname up.

Compose runs everything on one Docker host. For what containers isolate and where they differ from full VMs, see Docker vs a virtual machine.

Frequently asked questions

What is the difference between docker-compose and docker compose?
docker-compose with a hyphen is Compose v1, a separate standalone program that Docker no longer maintains. docker compose with a space is Compose v2, a plugin built into the Docker CLI and included with Docker Desktop. The file format is the same; use docker compose.
Should I still put version: "3.8" at the top of my compose file?
No. The top-level version key is obsolete in current Compose. It is only informative, and Compose prints a warning that it is obsolete when it finds one. Delete the line; nothing else changes.
Does docker compose down delete my database data?
Not by default. docker compose down removes the project's containers and networks but keeps named volumes. Only docker compose down -v (--volumes) also removes the named volumes declared in the file, which deletes data stored in them.
Is Docker Compose suitable for production?
Compose runs containers on a single Docker host. That is fine for small deployments, staging servers and many self-hosted apps. If you need to spread containers across several machines with automatic failover, you need an orchestrator such as Kubernetes instead.

Sources