What are Docker Compose profiles and how to use them?
Docker Compose profiles are a way to mark services as opt-in. By default, a service in compose.yaml starts whenever you run docker compose up. Adding profiles: ["name"] to a service flips that: the service stays dormant unless you explicitly activate that profile. This lets you keep one Compose file that covers "dev stack", "dev stack + debug tools", "dev stack + load tests", etc., without juggling multiple files.
Theory
TL;DR
- A service without
profilesis always active. - A service with
profiles: ["x"]is active only if profilexis enabled. - Enable via CLI:
docker compose --profile x up. - Or via env:
COMPOSE_PROFILES=x docker compose up. - Multiple profiles can be active at once (
--profile x --profile y). - A service can belong to multiple profiles (any one matching activates it).
- Dependencies (
depends_on) of a profiled service must also be in the profile or always-on; otherwise startup fails.
Why profiles
Real-world Compose stacks accumulate optional pieces:
- Database admin UIs (pgadmin, mongo-express, redis-commander)
- Mail catchers (mailhog, mailpit) for dev
- Load testing (k6, locust)
- Tracing/metrics (Jaeger, Prometheus, Grafana) you only run when investigating
- Tools-only profiles: a one-shot
migrateservice that runs DB migrations
Without profiles, choices are:
- Multiple files (
compose.yaml,compose.debug.yaml) merged with-f. Works, gets verbose. - Comment-out blocks. Sloppy, not version-friendly.
- Profiles: one file, one source of truth, opt-in execution.
Activation rules
- Compose collects all profiles activated via
--profileorCOMPOSE_PROFILES(comma-separated). - A service is selected if it has no
profiles:key OR if any of itsprofiles:values match an activated profile. - The remaining services are ignored (not built, not pulled, not started).
- Targeting a specific service by name auto-activates its profile:
docker compose up pgadminworks even without--profile debugifpgadminis in profiledebug.
Examples
Basic dev/debug split
# compose.yaml
services:
app:
image: myorg/app:1.0
ports: ["3000:3000"]
environment:
DATABASE_URL: postgres://app:app@db:5432/app
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
volumes:
- dbdata:/var/lib/postgresql/data
pgadmin:
image: dpage/pgadmin4
profiles: ["debug"]
environment:
PGADMIN_DEFAULT_EMAIL: admin@local.test
PGADMIN_DEFAULT_PASSWORD: admin
ports: ["5050:80"]
depends_on:
- db
volumes:
dbdata:Daily dev:
docker compose up
# starts app + db. pgadmin is dormant.When you need pgadmin:
docker compose --profile debug up
# starts app + db + pgadminOne-shot tool: migration runner
services:
app:
image: myorg/app:1.0
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
migrate:
image: myorg/app:1.0
profiles: ["tools"]
command: ["npm", "run", "migrate"]
depends_on:
db:
condition: service_healthy# Run migrations once
docker compose --profile tools run --rm migrateThe migrate service stays out of the regular up. Run it via --profile tools run --rm migrate and it runs once, exits, gone.
Multiple profiles per service
services:
jaeger:
image: jaegertracing/all-in-one
profiles: ["tracing", "observability"]
ports: ["16686:16686"]Activated by either --profile tracing or --profile observability.
Combining profiles
services:
app:
image: myorg/app:1.0
db:
image: postgres:16
redis:
image: redis:7
profiles: ["cache"]
worker:
image: myorg/app:1.0
command: ["npm", "run", "worker"]
profiles: ["workers"]
loadtest:
image: locustio/locust
profiles: ["perf"]# minimal
docker compose up
# with cache + workers
docker compose --profile cache --profile workers up
# perf benchmarks
docker compose --profile cache --profile workers --profile perf up
# via env (CI-friendly)
COMPOSE_PROFILES=cache,workers docker compose upProfiles + depends_on
services:
app:
depends_on: ["db"]
db:
image: postgres:16
pgadmin:
image: dpage/pgadmin4
profiles: ["debug"]
depends_on: ["db"] # OK: db is always-active
pgadmin-monitor:
image: myorg/pgadmin-monitor
profiles: ["debug"]
depends_on: ["pgadmin"] # OK: pgadmin is in same profileIf a profiled service depends on another profiled service that is not activated, Compose errors:
services:
app:
depends_on: ["redis"] # error: app is always-active, redis is profiled
redis:
profiles: ["cache"]Fix: either always-activate redis (remove its profile), or move app into the cache profile too.
CI-friendly: env-driven
# compose.yaml — same file used for dev and CI
services:
app:
image: myorg/app:1.0
db:
image: postgres:16
e2e:
image: myorg/e2e-tests
profiles: ["e2e"]
depends_on: ["app"]# .github/workflows/test.yml
- run: docker compose up -d # app + db
- run: COMPOSE_PROFILES=e2e docker compose run --rm e2e
- run: docker compose downDev gets a fast docker compose up; CI activates e2e to run the test suite.
Real-world usage
- Internal-tool inclusion: pgadmin, redis-commander, queue-monitor — opt-in.
- Heavy add-ons: ELK stack for logs, Jaeger for traces — turn on when investigating.
- One-shot runs: migrations, seed-data, cron-jobs —
profiles: ["tools"], run viacompose run --rm. - Multi-environment configs:
dev,staging,e2eprofiles, swapping which services participate. - Optional dependencies: caching layer that can be disabled in dev for speed.
Common mistakes
Forgetting depends_on constraints
services:
app:
depends_on: ["queue"]
queue:
profiles: ["workers"]docker compose up fails: app is always-active but its dependency is not. Either remove the profile from queue or move app to the same profile.
Using profiles when overrides are simpler
For environment-specific config (different image tags, different env vars), compose.override.yaml or -f compose.staging.yaml is more idiomatic. Profiles are best for opting in optional services, not transforming the same service across environments.
Activating profiles surprises
docker compose up pgadmin
# pgadmin's profile is auto-activated. Other dev tools in the same profile also start.If you mean to start only one profiled service, target it explicitly. If others in its profile come along, that is intentional (they share the profile).
Forgetting --profile on down
docker compose --profile debug up # 4 services running
docker compose down # only the always-active 3 are stoppeddown only stops what up would start with the current profiles. Match the profile flags on both ends, or use docker compose down --remove-orphans to catch the rest.
Follow-up questions
Q: Can a service be in multiple profiles?
A: Yes. Activating any one of them turns the service on.
Q: Does docker compose ps show profiled services that are dormant?
A: No, only services that are part of the current selection. To see all defined services, use docker compose config --services.
Q: Can a profile activate other profiles?
A: No. Profiles are a flat list. If you want a meta-profile, activate multiple via env var.
Q: What happens with docker compose build and profiles?
A: Same selection rules. Without --profile, only always-active services build. With --profile name, the matching profiled services build too.
Q: (Senior) When should profiles fall short, and what is the alternative?
A: Profiles are great for opt-in services. They struggle when you need different config for the same service across environments (different image tags, replicas, env vars). Then prefer multiple Compose files merged via -f compose.yaml -f compose.prod.yaml. Each file can override or extend the previous. Profiles + override files are not mutually exclusive; they solve different problems.
Q: (Senior) How do profiles interact with docker compose watch?
A: watch only monitors services that are part of the active selection. If you docker compose --profile debug watch, debug-only services are watched too. Without the profile, they are ignored. So profiles let you scope watch to the relevant subset.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.
Comments
No comments yet