✦ For everyone, free.

Practical knowledge for real and everyday life

Home

17.3 Compose Practices

A focused guide to Compose Practices, connecting core concepts with practical Docker and container operations.

Compose practices bring together the structural and organizational conventions that keep a Compose-based project maintainable as it grows, file layout, override usage, dependency health conditions, and resource scoping, distinct from any single troubleshooting concern, focused on how a Compose file should be structured from the outset to avoid accumulating the kind of confusion that ad hoc, organically grown configuration tends to produce.

Base file plus environment-specific overrides

Structuring a project around one base Compose file containing the service topology common to every environment, with separate, environment-specific override files supplying only what genuinely differs, keeps the bulk of the configuration in one place while still allowing legitimate per-environment variation:

services:
  api:
    build: .
    ports:
      - "3000:3000"
services:
  api:
    image: registry.example.com/my-api:1.4.2
    deploy:
      replicas: 3
    restart: unless-stopped
docker compose -f docker-compose.yml -f docker-compose.production.yml up -d

This pattern avoids maintaining several entirely separate, independently drifting Compose files for each environment, since most of the actual service definition lives once, in the base file, with overrides limited to genuinely environment-specific concerns.

Version controlling every Compose file, never the actual secrets

Every Compose file, base and override alike, should be committed to version control as part of the project, while secret values referenced within them should come from external secret files or a secrets manager, never embedded directly in any committed file:

services:
  api:
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt
secrets/

Excluding the actual secret files through .gitignore while committing the Compose file's reference to them keeps the structure documented and reviewable without ever risking a committed credential.

Health-aware dependency conditions as the default

Every depends_on entry referencing a service with meaningful startup time should specify a health condition rather than relying on the weaker, default start-order-only guarantee, since this single practice prevents a substantial share of startup-ordering related failures before they ever occur:

services:
  api:
    depends_on:
      db:
        condition: service_healthy
  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]

Using profiles to scope optional services

Compose profiles allow defining services that are only started when explicitly requested, useful for development-only tooling, debugging utilities, or services not needed in every invocation, without needing a separate Compose file just to exclude them by default:

services:
  api:
    image: my-api
  mailhog:
    image: mailhog/mailhog
    profiles:
      - debug
docker compose up -d
docker compose --profile debug up -d

This keeps a single Compose file expressive enough to cover both routine and occasional, optional needs, without forcing every invocation to bring up services that are not actually needed most of the time.

Explicit resource limits even in Compose-managed deployments

Resource limits, restart policies, and other runtime parameters covered generally elsewhere apply equally within Compose service definitions and should be configured explicitly rather than left at whatever default the underlying container runtime applies:

services:
  api:
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1"
      restart_policy:
        condition: on-failure

Naming services clearly and consistently

Service names should be clear, consistent, and free of ambiguity, since they double as the DNS-resolvable hostname every other service uses to reach them, and an unclear or inconsistent naming convention compounds confusion across every place that name is subsequently referenced:

services:
  api:
  worker:
  db:
  cache:
services:
  svc1:
  backend-thing:
  PostgresDB:

The first set is immediately clear about each service's actual role; the second mixes naming conventions and provides little useful information about what each service actually does, which becomes a growing source of friction as the file and the team working with it both grow.

Recognizing when Compose alone has been outgrown

Compose remains an excellent fit for single-host or small, fixed-topology deployments, but a project that genuinely needs automatic rescheduling across multiple hosts, sophisticated rolling update orchestration beyond what Compose directly provides, or dynamic, demand-based scaling, has likely outgrown what Compose alone is designed to handle, and recognizing this transition point, rather than attempting to recreate that missing functionality through increasingly elaborate custom scripting around Compose, leads to a more sustainable architecture.

docker compose ps

A growing collection of custom wrapper scripts attempting to add multi-host awareness, automatic failover, or dynamic scaling on top of Compose is usually a stronger signal to evaluate a fuller orchestration platform than to continue investing in scripting around Compose's own, deliberately more limited scope.

Common mistakes

  • Maintaining several entirely separate, independently drifting Compose files for each environment rather than a single base file with environment-specific overrides.
  • Embedding secret values directly within a committed Compose file rather than referencing external, gitignored secret files or a dedicated secrets manager.
  • Using depends_on without a health condition by default, relying on the weaker start-order-only guarantee for dependencies with meaningful startup time.
  • Adopting an inconsistent or unclear service naming convention, compounding confusion across every subsequent reference to those names.
  • Continuing to invest in increasingly elaborate custom scripting around Compose long after the project's actual orchestration needs have outgrown what Compose alone is designed to provide.

Compose practices that keep a project maintainable center on a clear base-plus-override file structure, consistent and meaningful naming, health-aware dependency conditions as the default rather than the exception, and an honest, periodic assessment of whether Compose's deliberately simpler scope still matches the project's actual, current orchestration needs.

Content in this section