← Back to the section

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_healthy guarantee that a dependent service starts after the database is truly ready.
  • volumes persist data across container restarts.
  • environment passes configuration — Spring Boot reads environment variables directly as properties.
  • docker compose down stops and removes the containers while keeping the volumes; down -v removes the volumes too.
  • The docker-compose.yml file is committed to the repository — the environment is reproducible on any machine.
  • 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 run flags and managing a container's lifecycle.