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
jobstop-level element the sameprofilessemantics, 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 everyup.docker compose config --serviceswith 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 bareup, and Compose reports the leftoveradminercontainer as an orphan because that service isn’t in the active model. It’s a warning, not an error — butdocker compose downwon’t clean it up either. Either keep the profile inCOMPOSE_PROFILESfor that project ordocker compose --profile debug downwhen 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 -qin 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. buildstill happens per service. A profiled service with abuild: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.