Docker Compose Profiles: Defaults, Dependencies, and Practical Patterns

Docker Compose Profiles: Defaults, Dependencies, and Practical Patterns

Quick answer

Quick takeaway: Docker Compose profiles let one Compose file describe both the required application stack and optional services. A service without profiles is enabled by default. A service with profiles is included only when a matching profile is active or when you explicitly target that service.[1][2]

That sounds simple until dependencies enter the model. Explicitly targeting a profiled service does not start every service in the same profile. Compose starts the target and its valid declared dependencies. If a required dependency is hidden behind an incompatible profile, Compose can reject the active model instead of guessing what you intended.[2][4]

OperationUnprofiled servicesMatching profiled servicesOther services in the target’s profile
docker compose upIncludedNot includedNot included
docker compose --profile debug upIncludedIncludedIncluded if they match debug
COMPOSE_PROFILES=debug docker compose upIncludedIncludedIncluded if they match debug
docker compose run migrateOnly valid dependencies needed by the targetThe targeted service is includedNot started merely because they share its profile

Start with a stable default stack

Put the services required for the application to function on the default path. Optional admin interfaces, debugging agents, data seeders, and one-off jobs are better profile candidates.

services:
  app:
    image: ghcr.io/example/app:latest
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: local-only-example
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10

  adminer:
    image: adminer:4
    depends_on:
      - db
    profiles: [debug]Code language: YAML (yaml)

With no profile, Compose selects app and db. The adminer service remains out of the model. That makes the plain command predictable for teammates who only need the application:

# Default stack: app + db
docker compose up -d

# Default stack plus Adminer
docker compose --profile debug up -d

# The environment-variable equivalent
COMPOSE_PROFILES=debug docker compose up -d

# Enable several optional groups
COMPOSE_PROFILES=debug,tools docker compose up -d

# Enable every declared profile. Quote the wildcard.
docker compose --profile "*" up -dCode language: Bash (bash)

Docker documents repeated --profile flags for multiple profiles and a comma-separated list for COMPOSE_PROFILES. It also documents the quoted "*" wildcard for enabling every profile.[1][3]

If docker compose up itself is unfamiliar, review what Docker Compose Up does before adding profile logic.

Inspect selection before starting containers

You do not need to infer the active model from a large YAML file. Ask Compose which profiles exist and which services the current command would select.

# List declared profile names
docker compose config --profiles

# Show default services
docker compose config --services

# Show services selected with one profile active
docker compose --profile debug config --services

# Check a CI profile supplied through the environment
COMPOSE_PROFILES=ci docker compose config --servicesCode language: Bash (bash)

Use this inspection step in reviews and pipeline debugging. It catches misspelled profile names, leaked environment variables, and missing dependencies before an up operation changes container state.

Profile names must match the Compose service reference pattern [a-zA-Z0-9][a-zA-Z0-9_.-]+.[1] Names such as debug, ci, and ops-tools are clear. Avoid names that mix environment, team, and lifecycle meanings into one label.

Choose a profile or another Compose mechanism

Profiles answer one question: should this service be enabled for this operation? They do not rewrite the service’s image, ports, resource limits, or secrets.

NeedBest starting choiceWhy
Keep an admin UI off by defaultProfileThe service is optional but its definition stays the same.
Run migrations or seed data on demandProfile plus explicit targetA one-off service can be invoked without bringing up its profile siblings.
Start a set of CI helpers togetherci profileThe group has one clear activation purpose.
Change images, ports, or resources by environmentCompose override or another configuration layerThis changes service configuration, not only selection.
Protect production from an unsafe toolDeployment controls, access controls, and separate configurationA profile is a convenience selector, not a security boundary.

A practical rule is to keep the minimum viable application path unprofiled. Add profiles around optional capabilities, not around every service.

Explicit targets activate narrowly

Suppose a repository defines two tools under the same profile:

services:
  db:
    image: postgres:16

  migrate:
    image: ghcr.io/example/app:latest
    command: ["./app", "migrate"]
    depends_on:
      - db
    profiles: [tools]

  seed:
    image: ghcr.io/example/app:latest
    command: ["./app", "seed"]
    depends_on:
      - db
    profiles: [tools]Code language: YAML (yaml)

You can target migrate without first passing --profile tools. Compose includes the targeted service and resolves its declared dependency on db. It does not start seed just because seed shares the tools profile.[1][2]

# Narrow invocation: migrate + its db dependency
docker compose run --rm migrate

# Different narrow invocation: seed + its db dependency
docker compose run --rm seed

# Group invocation: both tools-profile services plus defaults
docker compose --profile tools upCode language: Bash (bash)

This distinction is useful in CI. A migration check should not silently run every helper assigned to tools. If you need the full group, activate the profile. If you need one job, target that job.

Dependency behavior and invalid models

depends_on controls service dependency order and can add readiness conditions such as service_healthy. Profile selection happens at the application-model level. A dependency cannot be ordered or checked if it was removed from the active model.[2][4]

The following file contains a profile mismatch:

services:
  app:
    image: ghcr.io/example/app:latest

  test-lib:
    image: ghcr.io/example/test-lib:latest
    profiles: [test]

  debugger:
    image: ghcr.io/example/debugger:latest
    depends_on:
      - test-lib
    profiles: [debug]Code language: YAML (yaml)

With only debug active, debugger is selected but test-lib is not. The Compose Specification describes this model as invalid. Explicitly targeting debugger alone does not fix it because the target and dependency have no common profile.[2][4]

Dependent and dependency designResultRecommendation
Profiled service depends on an unprofiled core serviceValid default patternBest for databases, queues, and other required shared services.
Both services share a profileValid when that profile is active or the dependent is explicitly targetedGood for a tightly coupled optional group.
Services use different profiles and both profiles are activeValidUse when the groups are separate but intentionally combined.
Services use different profiles and only the dependent’s profile is activeInvalid modelActivate both, redesign the profiles, or keep the dependency unprofiled.
links, extends, or service:xxx points to a hidden serviceReference does not auto-enable itMake the referenced service active explicitly.

Use one of these deterministic fixes:

# Fix 1: activate both sides of the cross-profile dependency
docker compose --profile debug --profile test up debugger

# Fix 2: if test-lib is truly required infrastructure, remove its profiles key,
# then validate the resulting model before starting anything
docker compose --profile debug config --services
docker compose --profile debug up debuggerCode language: Bash (bash)

The same caution applies to shared service references through links, extends, and service:xxx. The profile reference says those links do not activate an otherwise ignored service. Compose returns an error instead.[2][4]

For startup readiness, pair a valid profile model with a healthcheck and an appropriate depends_on condition. The Docker Compose healthcheck guide covers that separate lifecycle concern.

A practical dev, CI, and operations layout

One file can express several optional paths without making the default path noisy:

services:
  app:
    build: .
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10

  mailpit:
    image: axllent/mailpit:latest
    profiles: [dev]

  adminer:
    image: adminer:4
    depends_on: [db]
    profiles: [dev]

  integration-tests:
    build: .
    command: ["./scripts/integration-test.sh"]
    depends_on:
      db:
        condition: service_healthy
    profiles: [ci]

  migrate:
    build: .
    command: ["./app", "migrate"]
    depends_on:
      db:
        condition: service_healthy
    profiles: [tools]

  metrics-exporter:
    image: example/metrics-exporter:1
    profiles: [ops]Code language: YAML (yaml)
ContextCommandSelected optional behaviorTeam rule
Everyday developmentdocker compose up -dNoneFast, stable core stack.
Developer toolingdocker compose --profile dev up -dMailpit and AdminerDo not put required app dependencies here.
CI integration testdocker compose run --rm integration-testsOne targeted CI job and valid dependenciesKeep the job deterministic.
All CI servicesdocker compose --profile ci up --abort-on-container-exitEntire CI profileUse only when the pipeline needs the group.
Migrationdocker compose run --rm migrateOne tool plus dependenciesPrefer explicit targets for one-off jobs.
Optional operations collectordocker compose --profile ops up -d metrics-exporterMetrics exporterTreat access and production configuration separately.

The production-oriented example is intentionally modest. Profiles can keep an optional service out of the default selection, but they do not make the service safe, authorized, or correctly configured for production.

Procedures teams can copy

Use a short validation routine before committing a new profile:

# 1. Render and validate the default model
docker compose config --services

# 2. Validate every named profile used by automation
docker compose --profile dev config --services
docker compose --profile ci config --services
docker compose --profile tools config --services

# 3. Validate the combined model when profiles depend on each other
docker compose --profile debug --profile test config --services

# 4. In CI, avoid inheriting a developer's profile selection
unset COMPOSE_PROFILES
COMPOSE_PROFILES=ci docker compose config --services

# 5. Clean up using the same intentional profile scope
docker compose --profile ci down --remove-orphansCode language: Bash (bash)

When choosing cleanup behavior, remember that profile selection and lifecycle commands are separate decisions. See Docker Compose down vs stop before standardizing the final command.

Also keep restart behavior separate from profile activation. A service being selected does not answer how its container should restart after failure. Use the Docker Compose restart policy guide for that decision.

Common failures

SymptomCauseFast checkFix
Optional service is missingNo matching profile is activedocker compose config --profilesAdd --profile name, set COMPOSE_PROFILES, or target the service.
A sibling in the same profile did not startYou targeted one service instead of enabling the profileCompare the command with config --servicesEnable the profile or target the sibling too.
Compose reports a dependency on an undefined serviceThe dependency was filtered by profile selectionRun config --services with the exact active profilesActivate both profiles, share a profile, or unprofile the core dependency.
--profile * selects unexpected filenames or failsThe shell expanded the wildcardInspect the invoked commandUse --profile "*".
CI starts local-only toolsCOMPOSE_PROFILES leaked into the job environmentPrint COMPOSE_PROFILES and selected servicesUnset it, then set the exact CI value.
A service starts before its dependency is readySelection is valid, but readiness is not definedInspect healthchecks and depends_on conditionsAdd a real healthcheck and service_healthy where appropriate.

Related reading

Sources

FAQ

What happens if I run Docker Compose without a profile?

Services without a profiles attribute are enabled. Services assigned only to inactive profiles are ignored. For a normal docker compose up, that means the unprofiled default stack starts while optional profiled services stay out.[1][2]

Can I enable multiple Docker Compose profiles?

Yes. Repeat --profile, such as --profile dev --profile tools, or set a comma-separated value such as COMPOSE_PROFILES=dev,tools. Use --profile "*" when you intentionally want every declared profile.[1][3]

Does targeting one service enable every service in its profile?

No. Compose runs the explicitly targeted profiled service and its valid declared dependencies. Other services that share the profile are not started unless you target them too or explicitly enable the profile.[1]

Can `depends_on` activate a service in another profile?

Not by itself. If the dependency is absent from the active model, Compose can reject the project. Keep core dependencies unprofiled, share a compatible profile, or activate every required profile in the same operation.[2][4]

Sergio Bremming Avatar