A real application rarely runs on its own. It needs a database, a message broker, a cache. Docker Compose lets you describe all of these services in a single file and start them together — with one command.
The problem: starting everything by hand is tedious
Picture a typical development environment: a Spring Boot application, Postgres, Kafka, Zookeeper. Without Compose, every team member starts each container by hand, passes the right environment variables, maps the ports, and watches the startup order. When you switch machines or move between projects, you repeat all of it from scratch.
The classic complaint is "it works on my machine". Most often the reason is that the local environment differs from a colleague's: a different version of Postgres, a different port, a different environment variable. Docker Compose solves exactly this problem: the environment configuration lives in a file in the repository and is reproducible on any machine.
The short formula: one docker-compose.yml file — one docker compose up command — a ready-to-go environment.
What Docker Compose is
Docker Compose is a tool for describing and running multi-container applications. You describe all the services you need, their settings, the connections between them, and the volumes in a single YAML file. Compose reads this file and manages the lifecycle of all the containers: start, stop, rebuild.
Starting with Docker Desktop and current versions of Docker Engine, Compose is built into the CLI as a plugin — the command looks like docker compose (without the hyphen).
The structure of docker-compose.yml
Let's look at a minimal example: a Spring Boot application + Postgres.
services:
db:
image: postgres:16
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD: secret
ports:
- "5432:5432" # host port : container port
volumes:
- postgres_data:/var/lib/postgresql/data # volume to persist data across restarts
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 5s
timeout: 5s
retries: 10
app:
image: myapp:latest # or build: . — to build from a Dockerfile
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/myapp
SPRING_DATASOURCE_USERNAME: myapp
SPRING_DATASOURCE_PASSWORD: secret
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy # wait until Postgres passes the healthcheck
volumes:
postgres_data:
Let's break down the key blocks.
services
services is the main section of the file. Each key inside it is a service name (db, app). This name is also the hostname inside the Compose network: from the app container you can reach the database simply by the name db. You don't need to hardcode any IP addresses — Compose creates the internal DNS record automatically.
image and build
image specifies which image to use. If the image still has to be built from a Dockerfile, use build: . instead of image (or alongside it to set a tag name). During development it is often more convenient to build the application image on the fly:
app:
build:
context: .
dockerfile: Dockerfile
environment
environment sets environment variables inside the container. Spring Boot reads environment variables and uses them as configuration properties: SPRING_DATASOURCE_URL maps to spring.datasource.url in application.properties.
A long list of variables is convenient to move into a .env file and reference it via env_file: .env.
ports
ports maps a port from the container to the host. The format is "host:container". If you don't need access to Postgres from the host (only from the application inside the Compose network), you can drop the ports block for the database — the containers still see each other by service name.
volumes
volumes in a service section mounts a volume or a host directory inside the container. In the example above, the named volume postgres_data stores Postgres data across restarts. Named volumes are declared in the top-level volumes section.
For development it is convenient to mount the directory with your code:
volumes:
- ./src:/app/src # code changes are immediately visible in the container
healthcheck
healthcheck defines a command that checks whether the service is ready. Compose runs it periodically inside the container and marks the service as healthy only after it passes successfully.
Without a healthcheck, the depends_on dependency triggers when the container is running, but Postgres is not yet accepting connections — the application crashes with a connection error. With a healthcheck and condition: service_healthy, Compose waits for the database to be truly ready.
depends_on
depends_on controls the startup order. In its simplest form:
depends_on:
- db
But this only guarantees that the db container is started before app. To guarantee readiness, use the extended variant with condition:
depends_on:
db:
condition: service_healthy
Core commands
# start all services in the background
docker compose up -d
# view logs of all services (or a specific one: ... logs db)
docker compose logs -f
# stop and remove the containers (volumes are kept)
docker compose down
# stop and remove the containers along with the volumes
docker compose down -v
# rebuild the image and restart
docker compose up -d --build
# run a command inside a running container
docker compose exec db psql -U myapp
Networks in Compose
By default, Compose creates one shared network for all the services in the file. All containers on this network see each other by service name. You can explicitly describe several networks — for example, to isolate part of the services:
services:
app:
networks:
- frontend
- backend
db:
networks:
- backend
networks:
frontend:
backend:
More on networks in the article Networking in Docker.
Example: Spring Boot + Postgres with a development profile
A complete docker-compose.yml for local development of a Spring Boot application:
services:
db:
image: postgres:16
environment:
POSTGRES_DB: orders
POSTGRES_USER: orders
POSTGRES_PASSWORD: dev_secret
ports:
- "5432:5432"
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U orders"]
interval: 5s
retries: 10
app:
build: .
environment:
SPRING_PROFILES_ACTIVE: dev
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/orders
SPRING_DATASOURCE_USERNAME: orders
SPRING_DATASOURCE_PASSWORD: dev_secret
SPRING_JPA_HIBERNATE_DDL_AUTO: validate
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy
volumes:
- ~/.m2:/root/.m2 # Maven cache across build restarts
volumes:
pg_data:
The docker compose up -d command brings up Postgres, waits for it to be ready, and only then starts the application. Logs from both containers are available through docker compose logs -f.
In short
- Docker Compose describes a multi-container environment in a single YAML file and starts it with one command.
- Services see each other by the names defined under
services:— DNS resolution works automatically. healthcheck+depends_on: condition: service_healthyguarantee that a dependent service starts after the database is truly ready.volumespersist data across container restarts.environmentpasses configuration — Spring Boot reads environment variables directly as properties.docker compose downstops and removes the containers while keeping the volumes;down -vremoves the volumes too.- The
docker-compose.ymlfile is committed to the repository — the environment is reproducible on any machine.
What to read next
- Networking in Docker — how networks between containers are built and how to isolate them.
- Volumes and data storage — types of volumes, bind mounts, and when to use each.
- Running containers —
docker runflags and managing a container's lifecycle.