Skip to main content
RunBook Academy

Docker & ContainersIX ยท Docker ComposeCompose profiles

Profiles โ€” optional services in one file

Foundationโฑ ~18 mindocker

What you'll learn

  • Assign profiles and predict exactly which services a given command starts
  • Explain why `docker compose down` can leave containers running
  • Handle `depends_on` edges that cross a profile boundary
  • Choose between profiles and override files for the job in front of you

Prerequisites

Verified against Docker Engine 29.x ยท Docker Engine 28.x ยท Docker Compose 2.x ยท containerd 2.x ยท runc 1.2.x ยท BuildKit 0.20+ ยท Linux kernel 5.15+ ยท Ubuntu 24.04 LTS ยท Debian 12 (Bookworm) ยท 2026-08-12

Not yet marked complete on this device.

A profile is a label on a service that decides whether that service is included when Compose resolves the project. It lets one file carry optional services โ€” debug tools, exporters, one-off jobs โ€” without starting them every time.

The rule is short enough to memorise and is the thing most people get backwards:

A service with no profiles: attribute is always enabled. A service with a profiles: attribute is enabled only when one of its profiles is active.

Enabling a profile adds services. It never removes any.

Defining profiles

name: shop

services:
  web:
    image: nginx:1.27
    ports: ["8080:80"]

  api:
    image: myorg/api:1.4.0

  # Opt-in: a Prometheus exporter nobody needs by default
  exporter:
    image: prom/node-exporter:v1.8.2
    profiles: [metrics]

  # Opt-in and dangerous: see the callout below
  debug-tools:
    image: myorg/debug-tools:1
    profiles: [debug]
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

web and api start on every docker compose up, with or without any profile flag. exporter starts only when metrics is active. debug-tools only when debug is.

Activating profiles

# Default: web and api only
docker compose up -d

# Add the debug profile
docker compose --profile debug up -d

# Multiple profiles
docker compose --profile debug --profile metrics up -d

# Everything, including every profiled service
docker compose --profile "*" up -d

The environment-variable form takes a comma-separated list and is what CI systems usually use, because it survives being set once for a whole job:

export COMPOSE_PROFILES=debug,metrics
docker compose up -d

Profile names must match [a-zA-Z0-9][a-zA-Z0-9_.-]+, which among other things means a single-character profile name is invalid.

Read-only / Safereconciliation check
PROJECT=shop

# What is actually running under this project, profiles or not
docker ps -a --filter "label=com.docker.compose.project=$PROJECT" \
--format '{{.Label "com.docker.compose.service"}}' | sort -u > /tmp/running.txt

# What a plain up would consider part of the project
docker compose config --services | sort > /tmp/declared.txt

comm -23 /tmp/running.txt /tmp/declared.txt

Anything printed by that comm is a container the daemon is running that a plain docker compose down will not touch. On a clean stack it prints nothing.

Dependencies across a profile boundary

Profiles cut depends_on edges, and the failure is loud in one direction and quiet in the other.

services:
  api:
    image: myorg/api:1.4.0
    depends_on:
      migrate:
        condition: service_completed_successfully

  migrate:
    image: myorg/api:1.4.0
    command: ["./migrate", "up"]
    profiles: [tools]     # <-- api can never start without --profile tools

api is always enabled and depends on a service that is not. A plain docker compose up -d cannot satisfy that dependency, so it fails rather than silently skipping it. The documented rule is that a gated dependency must be in the same profile as its dependent, started separately, or not profiled at all.

The useful half of the same mechanism: explicitly targeting a profiled service auto-enables its profiles, along with its depends_on dependencies. That is what makes one-off jobs pleasant:

Service impact possibleone-off
# Runs migrate and starts db, even though 'tools' was never enabled
docker compose run --rm migrate

Note the limit: only the targeted service and its declared dependencies start. Other services sharing the tools profile stay down unless they are targeted too or the profile is enabled outright.

What profiles are good for

  • Debug and admin tools โ€” a database console, a shell container, a profiler sidecar.
  • Optional observability โ€” exporters, tracing agents, log forwarders you want on some hosts and not others.
  • One-off jobs โ€” migrations, seed loaders, backup runners, invoked with docker compose run.
  • Alternate backends โ€” a real Postgres for integration tests versus an in-memory stand-in for unit tests.
  • Hardware-conditional services โ€” anything needing a GPU or a specific device that only some hosts have.

What profiles are not for

Environment differences. This is the common misuse and it does not work the way people hope:

# Wrong: a profile is not a security boundary and this file is in git
services:
  api:
    profiles: [prod]
    environment:
      DB_PASSWORD: "REPLACE_ME"

A profile decides whether a service is started. It does nothing to hide its configuration: anyone with the file can read the value, and docker compose --profile prod config will print it back for them. Nothing about a profile is a boundary.

The tool for environment differences is override files:

# compose.yml โ€” the shape of the application
services:
  api:
    image: myorg/api:1.4.0

# compose.prod.yml โ€” what is different in production
services:
  api:
    environment:
      DATABASE_URL: ${PROD_DATABASE_URL}
    deploy:
      resources:
        limits:
          memory: 2g
docker compose -f compose.yml -f compose.prod.yml up -d

The override file can live outside version control, be templated by your config management, or be generated per host. It also composes with profiles rather than competing with them โ€” a production override plus a metrics profile is a perfectly normal combination.

Knowledge check

Knowledge check ยท 5 questions

  1. Q1. Services without a `profiles:` key are skipped when you run `docker compose --profile debug up`.

  2. Q2. A stack was started with `docker compose --profile debug up -d`. A plain `docker compose down` is then run. What happens to the debug-profiled container?

  3. Q3. An always-enabled `api` service has `depends_on` a `migrate` service that is gated behind the `tools` profile. What happens on a plain `docker compose up -d`?

  4. Q4. Which are appropriate uses of profiles? Select all that apply.

  5. Q5. You set `COMPOSE_PROFILES=mtrics` (a typo) in a CI job. What does Compose do?

Passing score: 75%. Answers are checked in this browser.