Skip to content
C Codeloom
Docker

Docker Compose Profiles Tutorial: Optional Services Done Right

Use Docker Compose profiles to start only the services you need. Learn the syntax, common patterns, and pitfalls when mixing profiles with depends_on.

·4 min read · By Codeloom
Beginner 8 min read

What you'll learn

  • What Compose profiles are and why they exist
  • How to mark services as optional
  • How profiles interact with depends_on
  • Patterns for dev, test, and debug stacks
  • Common mistakes around default behavior

Prerequisites

  • Familiar with docker compose basics

A typical compose.yaml grows over time. Today it spins up your API and database. Next week someone adds Redis. Then a worker. Then a debugger, a mailcatcher, and a load tester. Before long, docker compose up boots a small datacenter on your laptop. Profiles are the Compose feature that lets you keep optional services in the same file without paying for them every time.

What and Why

A profile is a tag attached to a service. Services without any profile always start. Services with profiles only start when one of their profiles is explicitly activated. This means you can keep your full stack in one file but choose which slice to run.

The alternative is multiple compose files glued together with -f flags or environment overrides, which gets unwieldy fast. Profiles keep everything in one place.

Mental Model

  • A service with no profiles key: always part of the default stack.
  • A service with profiles: [debug]: only runs when you say --profile debug.
  • A service with profiles: [debug, test]: runs when either profile is active.
  • Activating multiple profiles at once: --profile debug --profile test.

Profiles compose additively. There is no concept of “exclude this profile.” The default stack is whatever has no profiles attached. Anything tagged is opt-in.

Hands-on Example

A small stack with an always-on API and database, plus optional Redis and a mail catcher.

services:
  api:
    image: node:20-alpine
    command: node server.js
    ports: ["3000:3000"]
    depends_on: [db]

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: secret

  redis:
    image: redis:7-alpine
    profiles: [cache]

  mailhog:
    image: mailhog/mailhog
    ports: ["8025:8025"]
    profiles: [dev]

  k6:
    image: grafana/k6
    profiles: [loadtest]
    command: run /scripts/load.js
    volumes:
      - ./scripts:/scripts
docker compose up
-> api, db

docker compose --profile cache up
-> api, db, redis

docker compose --profile dev up
-> api, db, mailhog

docker compose --profile dev --profile cache up
-> api, db, mailhog, redis

docker compose --profile loadtest run --rm k6
-> k6 (one-shot), api, db (deps)
What runs for each invocation

The day-to-day developer runs docker compose up and gets a minimal stack. The frontend folks who need a mail UI add --profile dev. The performance engineer adds --profile loadtest and runs a one-off k6 container.

You can also set the environment variable COMPOSE_PROFILES=cache,dev so teammates do not have to remember flags.

Common Pitfalls

Forgetting that depends_on does not auto-activate profiles. If api depends on redis and Redis is in the cache profile but api is not, then docker compose up will warn that api references a service that is not started. The fix is to either give api the same profile, or move Redis out of the profile, or activate the profile explicitly. Compose 2.x activates profiled dependencies automatically only when the depending service itself starts.

Expecting down to also stop profiled services. By default, docker compose down only touches whatever the current invocation knows about. If you started with --profile dev, you must include the same flag on down, or use --remove-orphans to sweep up.

Putting required services behind profiles “for cleanliness.” If your app cannot run without it, it belongs in the default set. Profiles are for genuinely optional things.

Using profiles to hide environment-specific config. Profiles are not a replacement for separate compose files when the differences are about images, env vars, or ports. Use overrides (compose.override.yaml) for that, profiles for optional services.

Practical Tips

Name profiles by intent, not by service. debug, test, loadtest, analytics read better than with-redis or extra-stuff.

Document profiles at the top of compose.yaml in a comment block. New contributors will not discover them otherwise.

Combine with run --rm for one-shot tools like k6 or a migration runner. Profile-gated services that you only ever invoke once are perfect for this pattern.

Set sensible defaults in a .env file: COMPOSE_PROFILES=dev means teammates get the dev tools without remembering the flag, while CI can pass an empty value for a minimal stack.

Audit periodically. If a profile has been activated by everyone every day for six months, consider removing the profile and just including the service.

Wrap-up

Profiles turn compose.yaml from a fixed stack definition into a flexible toolbox. The mental model is small — tag optional services, activate when needed — and it scales nicely as the file grows. Used well, profiles keep one source of truth for the whole stack while letting each developer or CI job run only what it needs.