Docker Compose

Docker Compose

Definition: A tool for defining and running multi-container Docker applications using a single YAML file, instead of manually running and wiring together multiple docker run commands by hand. It started life in 2013 as an independent project called Fig, built by Orchard, before Docker Inc. acquired the company in 2014 and rebranded it as Docker Compose. Compose V2 later rewrote the tool in Go and folded it directly into the Docker CLI as docker compose, replacing the original Python-based docker-compose binary while staying close to the same YAML format.

Core Services & Concepts

  • compose.yaml — the declarative file (historically docker-compose.yml) that describes every service an application needs — app, database, cache — and how they’re built, configured, and connected
  • Services — each named block in the file, mapping to one or more running containers built from an image or a local Dockerfile
  • Networking — Compose automatically creates a shared, project-scoped network so containers can reach each other by service name via built-in DNS, no manual IP wiring required
  • Volumes — named volumes or bind mounts that persist data (like a database’s files) outside a container’s own lifecycle, surviving down and restarts
  • depends_on — declares startup ordering between services, optionally gated on a condition: service_healthy check rather than just “container has started”
  • Environment files — .env files and the env_file key inject configuration and secrets into services without hardcoding them into the compose file itself
  • Multiple compose files — a base compose.yaml plus environment-specific overrides (compose.override.yml, or explicit -f flags) merged together at run time
  • Compose Watch — a newer feature that automatically syncs file changes or rebuilds a service’s image when source files change, closing the gap with dedicated dev-mode tooling

How It Works: Single-Host Multi-Container Orchestration

  • Compose reads one or more YAML files, merges them if multiple -f flags or an override file are present, and resolves the result into a single desired-state definition for the project
  • Every project gets a name (from the directory name or COMPOSE_PROJECT_NAME), used as a prefix for the containers, network, and volumes it creates, keeping multiple projects on one machine from colliding
  • A default bridge network is created per project, and every container on it can resolve every other service by name, e.g. an app service reaching postgres://db:5432 without knowing any container’s actual IP
  • depends_on controls startup order but, without an explicit healthcheck condition, only waits for a container to start rather than for the application inside it to be ready to accept connections
  • docker compose up reconciles the running containers against the file — creating, recreating, or leaving containers untouched depending on whether their config actually changed
  • Everything runs on a single Docker host/engine, there’s no scheduler placing containers across a fleet of machines the way Kubernetes or Swarm does

How Pricing Works

  • Docker Compose itself is free and open-source (Apache 2.0 license), distributed as a CLI plugin bundled with Docker Desktop and Docker Engine at no cost
  • The Compose Specification, the underlying YAML schema, is an open, vendor-neutral standard, not something Docker charges to use or implement
  • Docker Desktop — the application many developers use to run Compose locally — requires a paid subscription for larger companies (roughly 250+ employees or above a revenue threshold), while remaining free for individuals, small businesses, and education
  • Pulling images referenced in a compose file draws against Docker Hub’s pull-rate limits, which are more generous on paid Docker plans, an indirect cost for heavy CI usage
  • No usage-based billing exists for Compose itself, whatever cost exists comes from Docker Desktop licensing or the underlying image registry, not the orchestration tool

Pros

  • Spins up an entire multi-service local environment with a single command, docker compose up
  • Keeps a project’s full stack — app, database, cache, message queue — defined and version-controlled together in one readable file
  • Much simpler mental model and setup than Kubernetes for local development, small deployments, or CI test environments
  • Built directly into Docker Desktop and the Docker CLI, no separate install or cluster to stand up first
  • Easy to layer environment-specific overrides on top of a shared base file rather than maintaining several divergent configs

Cons

  • Single-host by design, it doesn’t span multiple machines or provide the scheduling Kubernetes offers across a cluster
  • No built-in auto-scaling or self-healing, a crashed container restarts per its restart policy but nothing redistributes load across hosts
  • depends_on without a healthcheck condition only confirms a container started, not that the service inside it is actually ready, a common source of flaky startup races
  • Secrets handling is minimal compared to a real secrets manager, .env files are easy to accidentally commit to version control
  • Not designed for production-scale orchestration, teams that outgrow a single server usually migrate to Kubernetes or a managed container platform

Comparison: Docker Compose vs Kubernetes vs Plain Dockerfile

Docker ComposeKubernetesPlain Dockerfile / docker run
ScopeMulti-container app, single hostMulti-container app, across a clusterSingle container image/build
OrchestrationBasic — start order, restart policyFull — scheduling, self-healing, auto-scalingNone, fully manual
Config formatOne YAML file per projectMultiple YAML manifests (or Helm charts)Dockerfile plus manual run flags
Best fitLocal dev, small deployments, CI test stacksProduction workloads needing scale and resilienceBuilding and testing a single image in isolation

Best For

  • Local development environments needing a database, cache, and app running together consistently across a team
  • Small, single-server production or staging deployments that don’t yet justify a full orchestrator
  • Ephemeral CI test environments that spin up a real database and dependent services for integration tests, then tear down

Real Examples

  • The standard way most full-stack open-source projects define “run the whole app locally” in a single command, from small side projects to large ones
  • Self-hosted deployment guides for tools like Sentry and Supabase ship an official compose.yaml as the recommended way to run the full stack on one server
  • Widely used in CI pipelines (GitHub Actions, GitLab CI/CD) to bring up a real Postgres or Redis instance for integration tests rather than mocking them

Use Cases

  • Local development environments that mirror a production stack’s services closely enough to catch integration bugs early
  • Small, single-server production deployments for low-traffic applications where Kubernetes would be significant overkill
  • Reproducible multi-service demos and proof-of-concept environments shared across a team
  • CI/CD pipelines that need a disposable, realistic set of dependent services (database, cache, queue) for automated tests
  • Onboarding new engineers quickly, git clone plus docker compose up replacing pages of manual environment setup instructions

Integration Notes & Common Pitfalls

  • Add explicit healthchecks and depends_on: condition: service_healthy rather than trusting default startup ordering, plain depends_on races are a frequent source of “works sometimes” bugs
  • Never commit .env files containing real secrets, use .env.example for documentation and keep actual secret values out of version control
  • Pin image tags to specific versions instead of latest, an unpinned base image changing underneath a team causes hard-to-reproduce inconsistencies
  • Remember docker compose down doesn’t remove named volumes by default, add -v explicitly when a truly clean slate (including data) is intended
  • Watch for the legacy top-level version: key, it’s deprecated in the modern Compose Specification and now just produces a warning rather than doing anything

Code Example

# compose.yaml — a typical app + database + cache stack
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/appdb
    depends_on:
      db:
        condition: service_healthy
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 5s
      retries: 5
    volumes:
      - db-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

volumes:
  db-data:

Code Example: Common CLI Workflow

# Start every service in the background
docker compose up -d

# Follow logs for just the app service
docker compose logs -f app

# Run a one-off command inside a running service's container
docker compose exec app npm run migrate

# Scale a stateless service to 3 replicas
docker compose up -d --scale app=3

# Tear everything down, including named volumes
docker compose down -v

# Rebuild and watch for live file sync during development
docker compose watch

Ecosystem

  • Docker Hub — the default public registry Compose pulls images from, hosting both official images (postgres, redis, nginx) and countless community ones
  • Compose Specification — the open, vendor-neutral schema Compose implements, maintained collaboratively rather than as a Docker-only proprietary format
  • Docker Desktop — the GUI application bundling Compose, the Docker engine, and Kubernetes support into one local development package
  • Compose Watch — the built-in live-reload feature that narrows the gap with dedicated dev-mode tools for containerized development
  • Testcontainers — a popular alternative/complement for spinning up dependent services programmatically inside test code rather than via a static compose file

Best Practices

  • Pin every image to a specific version tag, never rely on latest for anything beyond quick throwaway experiments
  • Add healthchecks to any service another one depends on, and gate depends_on on service_healthy rather than plain startup
  • Split configuration across a base compose.yaml and environment-specific override files instead of maintaining several near-duplicate full copies
  • Use named volumes for anything that needs to persist, and bind mounts only for source code you want to live-edit from the host
  • Keep secrets in .env files excluded from version control, or better, pull them from a real secrets manager in production-adjacent environments

FAQ

Is Docker Compose suitable for production? It can run small, single-server production workloads fine, but it lacks the self-healing, multi-host scheduling, and rolling-update capabilities of Kubernetes, so most teams migrate once they need real scale or high availability.

What’s the difference between docker-compose and docker compose? docker-compose (with a hyphen) was the original standalone Python V1 tool; docker compose (a space, V2) is the modern Go-based rewrite built directly into the Docker CLI, and it’s the version actively maintained today.

How do containers in a Compose project talk to each other? Through a shared, automatically created network where each service is reachable by its service name via built-in DNS, no manual IP configuration needed.

Does docker compose down delete my data? Not by default, named volumes persist unless -v is explicitly passed, at which point volumes attached to the project are removed along with the containers and network.

Can Compose deploy across multiple servers? Not on its own, a compose file is scoped to a single Docker host, though the same file format can be adapted for Docker Swarm’s docker stack deploy, which does support multiple nodes.

Common Interview Questions

  • “What are the limitations of depends_on in Docker Compose?” — expect an answer distinguishing container-started from application-ready, and the role of healthchecks in closing that gap
  • “How does service discovery work inside a Compose project?” — expect an explanation of the automatically created network and DNS resolution by service name
  • “What’s the difference between a named volume and a bind mount?” — expect a distinction between Docker-managed storage and a direct host filesystem path mounted into the container
  • “When would you reach for Kubernetes instead of Docker Compose?” — expect discussion of multi-host scaling, self-healing, and production resilience requirements Compose doesn’t address

History

  • Started in 2013 as Fig, an independent project built by Orchard (Ben Firshman and Aanand Prasad among its creators) to simplify running multi-container Docker apps
  • Docker Inc. acquired Orchard in 2014 and rebranded Fig as Docker Compose, folding it into the official Docker toolchain
  • Compose V2 rewrote the tool in Go and integrated it as a Docker CLI plugin (docker compose), eventually deprecating the standalone Python V1 binary
  • The Compose Specification was opened up as a vendor-neutral standard, decoupling the file format itself from any single implementation
  • Compose Watch shipped as a built-in live-reload feature, extending Compose further into the local development workflow rather than just startup orchestration

Dig deeper