Skip to content

Docker Compose

Docker Compose is a tool for defining and running multi-container applications using a declarative YAML file. Instead of running multiple docker run commands with flags, you describe the entire application stack — services, networks, volumes, environment variables — in a single compose.yaml, and start everything with one command.


Basic structure

compose.yaml
name: myapp

services:
  web:
    build: ./web          # build from a local Dockerfile
    ports:
      - "80:80"
    depends_on:
      - app

  app:
    build: ./app
    environment:
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/myapp
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: secret
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

All services share a default bridge network. Each service is reachable from other services using its service name as the hostname (db, app, web).

flowchart LR
    user[User] -->|HTTP :80| web[Web]
    subgraph myapp [172.18.0.0/16]
        web
        app[App]
        db[(Database :5432)]
    end
    web -->|API| app
    app -->|JDBC| db

Key commands

Command Description
docker compose up Create and start all services
docker compose up -d Start in detached (background) mode
docker compose up --build Rebuild images before starting
docker compose down Stop and remove containers and networks
docker compose down -v Also remove named volumes
docker compose logs -f Follow logs from all services
docker compose logs -f app Follow logs from a specific service
docker compose ps List running services
docker compose exec app sh Open a shell in a running service
docker compose restart app Restart a single service

Environment variables

Hard-coding credentials inside compose.yaml is a security risk and prevents environment-specific configuration. Use .env files instead:

compose.yaml
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-myapp}       # (1)!
      POSTGRES_USER: ${POSTGRES_USER:-myapp}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}   # (2)!
    volumes:
      - ${VOLUME:-./data}/db:/var/lib/postgresql/data
    ports:
      - "5432:5432"                             # (3)!
  1. :-myapp is the default value if POSTGRES_DB is not set in the environment or .env file. See Compose interpolation docs.
  2. No default — Compose will fail loudly if this variable is missing, preventing silent misconfiguration.
  3. In production, remove this port mapping so the database is only reachable inside the Compose network.
.env
POSTGRES_DB=myapp_prod
POSTGRES_USER=myapp
POSTGRES_PASSWORD=S3cr3t!
VOLUME=/data/myapp

When you run docker compose up, Compose automatically reads .env from the same directory as compose.yaml.

Never commit .env to version control

Add .env to .gitignore. Each environment (development, staging, production) should have its own .env file, kept outside the repository. Committing credentials — even to a private repo — is a security incident waiting to happen.


Health checks and dependency ordering

depends_on ensures a service starts after its dependencies, but does not wait for the dependency to be ready (e.g., the database to finish initialising). Use healthcheck for that:

compose.yaml
services:
  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myapp"]
      interval: 5s
      timeout: 5s
      retries: 5

  app:
    build: ./app
    depends_on:
      db:
        condition: service_healthy    # wait until db passes healthcheck

Named volumes vs. bind mounts

compose.yaml
volumes:
  db-data:        # Docker-managed named volume — data persists across `down`

services:
  db:
    volumes:
      - db-data:/var/lib/postgresql/data     # named volume (production)
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql  # bind mount (seed data)
Named volume Bind mount
Data location Managed by Docker Host path you specify
Persistence Survives docker compose down Survives (it's your host path)
Use case Production data, databases Dev: hot-reload source, seed scripts

Full example

compose.yaml
name: myapp

services:

  web:
    build:
      dockerfile_inline: |
        FROM nginx:latest
        RUN apt update
        RUN apt install -y net-tools iputils-ping
    ports:
      - 80:80

  app:
    image: eclipse-temurin:25-jdk
    depends_on:
      - db

  db:
    image: postgres:latest
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    volumes:
      - ${VOLUME}/db:/var/lib/postgresql/data
    ports:
      - 5432:5432

Try it:

docker compose up -d --build  # (1)!
  1. -d runs the containers in detached mode.
    --build forces a rebuild of images before starting.

Access the web service at http://localhost:80 and verify all containers are running with docker compose ps.