Quick answer
Quick takeaway: Docker Compose profiles let one Compose file describe both the required application stack and optional services. A service without
profilesis enabled by default. A service withprofilesis 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]
| Operation | Unprofiled services | Matching profiled services | Other services in the target’s profile |
|---|---|---|---|
docker compose up | Included | Not included | Not included |
docker compose --profile debug up | Included | Included | Included if they match debug |
COMPOSE_PROFILES=debug docker compose up | Included | Included | Included if they match debug |
docker compose run migrate | Only valid dependencies needed by the target | The targeted service is included | Not 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.
| Need | Best starting choice | Why |
|---|---|---|
| Keep an admin UI off by default | Profile | The service is optional but its definition stays the same. |
| Run migrations or seed data on demand | Profile plus explicit target | A one-off service can be invoked without bringing up its profile siblings. |
| Start a set of CI helpers together | ci profile | The group has one clear activation purpose. |
| Change images, ports, or resources by environment | Compose override or another configuration layer | This changes service configuration, not only selection. |
| Protect production from an unsafe tool | Deployment controls, access controls, and separate configuration | A 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 design | Result | Recommendation |
|---|---|---|
| Profiled service depends on an unprofiled core service | Valid default pattern | Best for databases, queues, and other required shared services. |
| Both services share a profile | Valid when that profile is active or the dependent is explicitly targeted | Good for a tightly coupled optional group. |
| Services use different profiles and both profiles are active | Valid | Use when the groups are separate but intentionally combined. |
| Services use different profiles and only the dependent’s profile is active | Invalid model | Activate both, redesign the profiles, or keep the dependency unprofiled. |
links, extends, or service:xxx points to a hidden service | Reference does not auto-enable it | Make 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)
| Context | Command | Selected optional behavior | Team rule |
|---|---|---|---|
| Everyday development | docker compose up -d | None | Fast, stable core stack. |
| Developer tooling | docker compose --profile dev up -d | Mailpit and Adminer | Do not put required app dependencies here. |
| CI integration test | docker compose run --rm integration-tests | One targeted CI job and valid dependencies | Keep the job deterministic. |
| All CI services | docker compose --profile ci up --abort-on-container-exit | Entire CI profile | Use only when the pipeline needs the group. |
| Migration | docker compose run --rm migrate | One tool plus dependencies | Prefer explicit targets for one-off jobs. |
| Optional operations collector | docker compose --profile ops up -d metrics-exporter | Metrics exporter | Treat 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
| Symptom | Cause | Fast check | Fix |
|---|---|---|---|
| Optional service is missing | No matching profile is active | docker compose config --profiles | Add --profile name, set COMPOSE_PROFILES, or target the service. |
| A sibling in the same profile did not start | You targeted one service instead of enabling the profile | Compare the command with config --services | Enable the profile or target the sibling too. |
| Compose reports a dependency on an undefined service | The dependency was filtered by profile selection | Run config --services with the exact active profiles | Activate both profiles, share a profile, or unprofile the core dependency. |
--profile * selects unexpected filenames or fails | The shell expanded the wildcard | Inspect the invoked command | Use --profile "*". |
| CI starts local-only tools | COMPOSE_PROFILES leaked into the job environment | Print COMPOSE_PROFILES and selected services | Unset it, then set the exact CI value. |
| A service starts before its dependency is ready | Selection is valid, but readiness is not defined | Inspect healthchecks and depends_on conditions | Add a real healthcheck and service_healthy where appropriate. |
Related reading
- Docker Compose Up: What It Does + Common Flags + Troubleshooting
- Docker Compose Down vs Stop
- Docker Compose Healthcheck
Sources
- Docker Docs: Using profiles with Compose
- Docker Docs: Compose file profiles reference
- Docker Docs: Pre-defined Compose environment variables
- Compose Specification: Profiles
FAQ
What happens if I run Docker Compose without a profile?
Can I enable multiple Docker Compose profiles?
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]






