Skip to content
Blog

Docker Compose Profiles: Running Only the Services You Need

Compose profiles let one compose.yaml hold your core stack plus every debug, admin, and observability sidecar — and start only the subset you ask for.

Published on • October 31, 2026

AI Assistant

A Compose file grows monotonically. The core stack is three services on day one; eighteen months later it also contains a database admin UI you use twice a week, an OpenTelemetry collector nobody runs locally, a seed-data job, and a mock payment worker that only matters in development. The usual fixes are worse than the disease: commented-out service blocks, -f compose.yaml -f compose.debug.yaml chains, or three drifting files per environment.

Profiles solve this with one attribute. A service assigned to a profile is invisible to Compose unless that profile is active; a service with no profile attribute always starts. One file, one model, selective activation.

Declaring profiles

The profiles attribute takes a list of profile names, as documented in the Compose services reference:

services:
  web:
    build: ./web
    ports: ["8080:80"]
    depends_on: [api]

  api:
    build: ./api
    depends_on: [db]

  db:
    image: postgres:18
    environment:
      POSTGRES_PASSWORD: dev

  adminer:
    image: adminer
    ports: ["8081:8080"]
    depends_on: [db]
    profiles: [debug]

  pgadmin:
    image: dpage/pgadmin4
    profiles: [debug]

  otel-collector:
    image: otel/opentelemetry-collector-contrib
    profiles: [observability]

  seed:
    build: ./api
    command: ./seed.sh
    profiles: [tools]

Here web, api, and db have no profiles key, so plain docker compose up -d starts exactly those three. adminer, pgadmin, otel-collector, and seed are parked behind profiles and cost nothing — Compose doesn’t build, pull, or plan them.

Profile names must match [a-zA-Z0-9][a-zA-Z0-9_.-]+. A service can belong to several profiles (profiles: [debug, tools]), which is how you express “this thing is both a debug aid and a test fixture”.

Activating profiles

Two mechanisms, both per-invocation:

docker compose --profile debug up -d        # flag
COMPOSE_PROFILES=debug docker compose up -d # environment variable

Enable several at once by repeating the flag or comma-separating the variable:

docker compose --profile debug --profile observability up -d
COMPOSE_PROFILES=debug,observability docker compose up -d

And enable everything in the file with a wildcard:

docker compose --profile "*" up -d

COMPOSE_PROFILES is the ergonomic choice for persistent configuration — put it in .env next to the Compose file (Compose reads it for interpolation) or export it in a make dev-obs target so every teammate gets the same stack. The flag is better for one-off sessions: docker compose --profile tools run seed.

The same flag applies to down:

docker compose --profile debug down   # stops debug services AND the unprofiled ones
docker compose stop adminer           # or stop one service by name

Note what that means: docker compose down with no profile active does not touch profiled services. If you started adminer last Tuesday and run a bare down today, it keeps running.

Dependency auto-enable semantics

This is where profiles get subtle, and where most confusion comes from.

Targeting a service auto-enables its own profiles — and only its own. Because adminer is named directly on the command line, Compose runs it even though debug was never enabled, and it pulls in db via depends_on:

docker compose up -d adminer   # runs adminer + db, no --profile needed
docker compose run seed        # same for one-off tools

But a profiled dependency is not pulled along. Compose auto-enables the profiles of services you name, never the profiles of services those names depend on. If zot has profiles: [debug] and depends_on: [bar] while bar has profiles: [tools], then:

docker compose up -d zot
# ERROR: Service "bar" was pulled in as a dependency of service "zot"
#        but is not enabled by the active profiles.

You have exactly three fixes, and the docs list them: put the dependency in the same profile, enable its profile explicitly, or leave it unassigned so it’s always enabled:

bar:
  image: bar
  profiles: [tools, debug]   # shares a profile with zot
docker compose --profile tools up -d zot   # or activate it yourself

The practical rule: the core services a profiled tool depends on — databases, brokers — should carry no profile at all. That matches Docker’s own tip that core services shouldn’t be assigned profiles. Profile the optional leaf, never the shared foundation.

One more wrinkle: auto-enable doesn’t recurse. With three levels (app → setup → test-db), naming setup enables setup’s profiles but not test-db’s, so deep dependency chains still need a shared profile or an explicit --profile.

References via links, extends, or service:xxx network modes behave differently again — the Compose specification says Compose returns an error rather than silently activating a component the active profiles would otherwise ignore.

What profiles are good for

  • Debug and admin UIs — Adminer, phpMyAdmin, pgAdmin, RedisInsight, Mongo Express. Tools with a web UI and an exposed port that you don’t want reachable in every environment.
  • Observability sidecars — an otel-collector, Jaeger, Prometheus, or a Vector agent that only makes sense when you’re actively tracing.
  • Development-only workers — a mail catcher (Mailpit), a Stripe mock, a seed/fixture loader, a dev search indexer.
  • Test and CI extras — a Selenium grid or a second app instance used only by integration tests. Note the spec also gives the jobs top-level element the same profiles semantics, so scheduled or one-shot jobs can be gated identically.
  • Per-developer variation — one teammate runs COMPOSE_PROFILES=debug, another doesn’t, and nobody edits the file.

Gotchas

  • Unprofiled services always start. Forgetting profiles: on an optional service is the classic mistake — the collector you meant to gate spins up on every up. docker compose config --services with and without --profile "*" shows the difference between your full model and your default one.
  • Orphan warnings after a profiled run. Start with --profile debug, then run a bare up, and Compose reports the leftover adminer container as an orphan because that service isn’t in the active model. It’s a warning, not an error — but docker compose down won’t clean it up either. Either keep the profile in COMPOSE_PROFILES for that project or docker compose --profile debug down when you’re done.
  • Ignored services aren’t validated with your stack. A broken port mapping in a service nobody activated fails only when someone activates it. Worth running docker compose --profile "*" config -q in CI to lint the whole file.
  • Profiles are not environment overrides. They select which services exist in the model, not how a service is configured. For configuration changes, override files (-f compose.yaml -f compose.dev.yaml) remain the right tool. The two compose cleanly: profiles gate services, overrides tune them.
  • build still happens per service. A profiled service with a build: section is only built when it’s in the active model — one of the quiet wins of doing this properly.

Sources and spec notes

Profiles are part of the Compose Specification, not a Docker-specific extension, so podman-compose and other implementations can support the same attribute — though activation mechanisms (--profile, COMPOSE_PROFILES) are implementation-specific. All other top-level elements (networks, volumes, configs, secrets) are unaffected by profiles and always active.

Further reading