Skip to main content

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 profiles is always active.
  • A service with profiles: ["x"] is active only if profile x is 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 migrate service that runs DB migrations

Without profiles, choices are:

  1. Multiple files (compose.yaml, compose.debug.yaml) merged with -f. Works, gets verbose.
  2. Comment-out blocks. Sloppy, not version-friendly.
  3. Profiles: one file, one source of truth, opt-in execution.

Activation rules

  • Compose collects all profiles activated via --profile or COMPOSE_PROFILES (comma-separated).
  • A service is selected if it has no profiles: key OR if any of its profiles: 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 pgadmin works even without --profile debug if pgadmin is in profile debug.

Examples

Basic dev/debug split

yaml
# 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:

bash
docker compose up # starts app + db. pgadmin is dormant.

When you need pgadmin:

bash
docker compose --profile debug up # starts app + db + pgadmin

One-shot tool: migration runner

yaml
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
bash
# Run migrations once docker compose --profile tools run --rm migrate

The 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

yaml
services: jaeger: image: jaegertracing/all-in-one profiles: ["tracing", "observability"] ports: ["16686:16686"]

Activated by either --profile tracing or --profile observability.

Combining profiles

yaml
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"]
bash
# 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 up

Profiles + depends_on

yaml
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 profile

If a profiled service depends on another profiled service that is not activated, Compose errors:

yaml
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

yaml
# 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"]
yaml
# .github/workflows/test.yml - run: docker compose up -d # app + db - run: COMPOSE_PROFILES=e2e docker compose run --rm e2e - run: docker compose down

Dev 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 via compose run --rm.
  • Multi-environment configs: dev, staging, e2e profiles, swapping which services participate.
  • Optional dependencies: caching layer that can be disabled in dev for speed.

Common mistakes

Forgetting depends_on constraints

yaml
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

bash
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

bash
docker compose --profile debug up # 4 services running docker compose down # only the always-active 3 are stopped

down 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Comments

No comments yet