Compose Profiles

Compose Profiles

One Compose file, many possible stacks — profiles let a single definition serve a lean local setup, a full integration-test environment, and a debugging toolkit, all without maintaining three separate files.

Anyone who has worked on a Compose file for long enough eventually hits the same tension: the file needs to describe the application’s core services, but it also grows to include things that are only sometimes wanted — an admin dashboard for local debugging, a mail-catcher tool for testing email flows, a heavier monitoring stack that would be wasteful to run on every single docker compose up. Before profiles existed, teams solved this by splitting logic across multiple Compose files, commenting services in and out by hand, or maintaining separate override files that had to be remembered and combined correctly every time. Profiles solve this directly: they let every service in a single Compose file declare which named “profile” or profiles it belongs to, and then let whoever runs the stack decide, at the command line, which of those profiles should actually be turned on. This guide covers what a profile really is, how Compose decides what to start, and the patterns that make this feature genuinely useful rather than just one more configuration knob.

ACore Concepts

Before looking at the mechanics, it helps to be precise about what a profile actually represents.

What a Compose profile is

A profile is simply a label a service in a Compose file can be tagged with. A service can have no profiles at all, one profile, or several. Services with no profile assigned are treated as core, always-on services, and Compose starts them by default every time the stack runs. Services that do have a profile assigned are only started when that specific profile is explicitly activated.

Why this matters beyond convenience

Without profiles, “optional” services usually end up either always running, wasting resources and slowing startup for people who do not need them, or manually commented out, which is easy to forget and easy to get wrong. Profiles turn “which services should run right now” from a manual editing task into a single, explicit, repeatable choice made at the moment the stack is started.

Activation is opt-in, not opt-out

The default behavior — services without a profile always start, services with a profile only start when requested — is intentional and worth internalizing early. It means adding a profile to a service is a way of saying “this is optional,” not a way of hiding a service that would otherwise be considered mandatory.

Everyday Analogy

Think of a Compose file as a restaurant’s full menu, and profiles as meal packages a customer can order. The kitchen always prepares the standard bread and water for every table — the core services, no profile needed. But the “dessert package” or the “wine pairing” only gets prepared if a customer specifically asks for that package by name. The same kitchen, the same full menu, produces a very different table setting depending on which packages were actually requested.

BArchitecture & Components

A few distinct pieces work together to make profile-based activation possible.
Compose File

The single source of truth

One YAML file describing every possible service the stack could run, with some services marked as belonging to one or more profiles.

profiles: key

The label on a service

A list attached to an individual service entry, naming which profile or profiles that service belongs to. A service can belong to several profiles at once.

–profile flag

Activation at the command line

A flag passed to Compose commands that explicitly turns on one or more named profiles for that run, in addition to whatever core services always start.

COMPOSE_PROFILES variable

Activation via environment

An environment variable that achieves the same effect as the command-line flag, useful for setting a standing default in a shell, a CI job, or an .env file.

Core (unlabeled) Services

The always-on baseline

Any service with no profiles entry, which Compose starts regardless of which profiles, if any, were activated.

Dependency Resolution

What happens with depends_on

Compose’s logic for handling a core or activated service that depends on a service belonging to a profile that was not activated — an important edge case covered in the next chapter.

CInternal Working

Here is what Compose actually does, step by step, when it decides which services to start.

When a Compose command runs, the tool first reads the entire file and builds a complete picture of every defined service. It then looks at whichever profiles were activated for this particular run — through the --profile flag, the COMPOSE_PROFILES variable, or both combined. Compose then filters the full service list down to exactly two groups: every service with no profile assigned at all, and every service whose profile list includes at least one of the activated profiles. Everything else is simply left out of this run entirely, as if it were not in the file.

How dependencies interact with profiles

If a core, always-on service declares a dependency on a service that belongs to a profile the user did not activate, Compose generally still starts that dependency, because the explicit dependency relationship overrides the profile filtering for that specific link. This detail matters: it means adding a profile to a service does not fully guarantee that service will never start, if something else in the stack genuinely depends on it.

Activating more than one profile at once

Multiple profiles can be activated in a single command, and Compose simply unions the results — every service belonging to any of the requested profiles gets included alongside the core services. This is what allows combinations like “start the core stack plus the debugging tools plus the seed-data helper” in one command, without needing a profile that was specifically designed to cover exactly that combination.

i
Key Distinction

Profiles control which services Compose starts for a given run — they are not a way to hide a service’s definition or its configuration. Anyone with the Compose file can see every service and every profile assignment; profiles only change what actually gets launched.

DData Flow & Lifecycle

Tracing one command from the terminal through to a running set of containers shows the filtering logic in action.

Imagine a Compose file describing a core web application and database with no profile assigned, plus an admin dashboard tagged with the profile debug, and a load-testing tool tagged with the profile testing.

flowchart TD
    A["Developer runs: docker compose --profile debug up"] --> B["Compose reads full file: web, db, admin-dashboard, load-tester"]
    B --> C["Compose checks each service's profile assignment"]
    C --> D["web: no profile - always included"]
    C --> E["db: no profile - always included"]
    C --> F["admin-dashboard: profile debug - matches activated profile, included"]
    C --> G["load-tester: profile testing - not activated, excluded"]
    D --> H["Compose starts web, db, admin-dashboard"]
    E --> H
    F --> H
    G --> I["load-tester is not started this run"]
    

Fig. 1 — Compose filters the full set of defined services down to core services plus anything matching an activated profile; everything else is skipped entirely for that run.

Notice that the load-testing tool was never touched, started, or even briefly initialized — from Compose’s point of view for this particular run, it might as well not be in the file. Running the exact same command again later with --profile testing instead would flip that outcome: the load-tester would start and the admin dashboard would be the one left out, all from the same unchanged Compose file.

Combining profiles with COMPOSE_PROFILES in CI

A continuous integration pipeline can set COMPOSE_PROFILES=testing once in its environment configuration, so every Compose command run during that pipeline automatically includes the testing-tagged services, without needing to remember to add a flag to every individual command in the pipeline script.

EAdvantages, Disadvantages & Trade-offs

Strengths

  • One Compose file can serve many different situations — local development, CI, full integration testing — without duplicating configuration
  • Optional, resource-heavy services no longer start by accident just because they exist in the file
  • Activation is explicit and visible in the command or environment, making it easy to see exactly what a given run will include

Limitations

  • A large Compose file with many profiles can become harder to read, since a service’s true activation conditions require checking its profile list carefully
  • Dependency relationships can quietly pull in a profiled service anyway, which can surprise someone expecting strict profile isolation
  • Profiles are a startup-time filter, not an access-control mechanism — they say nothing about network isolation or permissions once a service is running

The trade-off in one sentence: profiles trade a small amount of added file complexity for a large reduction in the number of Compose files (and the drift between them) a team would otherwise need to maintain by hand.

FSecurity

Profiles are a convenience feature for local and CI environments, and it is worth being explicit about what they do and do not protect against.

A profile determines whether Compose starts a service for a given run — it is not a permissions system, and it provides no runtime enforcement once containers are up. If a debugging or admin-tooling service tagged with a profile is ever activated in a shared or production-adjacent environment, it runs with exactly the same network access and privileges it would have anywhere else. Nothing about the profile mechanism itself restricts what that service can reach or who can reach it.

!
Common Misconception

Tagging a powerful debugging tool with a profile and assuming it is therefore “safe” because it does not start by default is a partial protection at best. If COMPOSE_PROFILES is ever set broadly in a shared environment, or someone runs a command with an overly permissive profile flag out of habit, that tool starts with full access, exactly as if no profile had been used at all.

The safer approach is to treat profiles purely as a convenience for controlling which services start in development and testing contexts, and to rely on separate, genuine safeguards — distinct Compose files or environment-specific configuration, network policies, and deployment pipelines that simply never reference risky profiles at all — for anything approaching production. A profile name should never be the only thing standing between a debugging tool and a live environment.

GDesign Patterns & Anti-patterns

PATTERN-01 Recommended
Pattern

Grouping optional developer tooling — admin dashboards, mail catchers, database GUIs — under a single shared profile such as debug or dev-tools.

Why it works

It gives every developer a single, memorable flag to add when they want the full convenience toolkit, without those services adding overhead for anyone who is not actively using them.

PATTERN-02 Recommended
Pattern

Giving CI-only services (integration test runners, seed-data loaders, mock external services) their own dedicated profile, activated exclusively through COMPOSE_PROFILES in the pipeline environment.

Why it works

It keeps CI-specific machinery entirely out of a normal local development run, while still keeping the definitions in the same shared Compose file everyone already works from.

ANTI-01 Avoid
Anti-pattern

Relying on a profile as the sole safeguard preventing a sensitive or destructive tool from ever running outside local development.

Why it causes problems

As covered in the security chapter, a profile is a startup filter, not an access boundary — anything that can set an environment variable or pass a flag can activate it anywhere the Compose file is used.

ANTI-02 Avoid
Anti-pattern

Creating one profile per individual service rather than grouping related optional services together.

Why it causes problems

It forces anyone who wants “the usual debugging setup” to remember and type a long list of individual profile names every time, defeating much of the convenience profiles are meant to provide.

HBest Practices & Common Mistakes

Do

Name profiles by purpose, not by service

Names like debug, testing, or monitoring communicate intent far better than a profile named after one specific tool.

Do

Document available profiles somewhere visible

A short note in the project’s README listing each profile and what it activates saves teammates from having to read the entire Compose file to discover what exists.

Do

Double-check dependency chains around profiled services

Before assuming a profiled service will never start automatically, verify nothing outside its profile depends on it directly.

Avoid

Setting COMPOSE_PROFILES globally and forgetting about it

A broad, forgotten environment setting can silently activate profiles in every Compose command run afterward, including ones where that was never intended.

Avoid

Treating “no profile” as “hidden” or “disabled”

A service with no profile is a core, always-on service by default — the opposite of hidden. Forgetting this is a common source of surprise resource usage.

Avoid

Overusing profiles instead of separate environment files for genuinely different deployments

Profiles are best for optional additions to one coherent stack, not for describing fundamentally different deployment targets that need different images, networks, or resource limits entirely.

IReal-World & Industry Examples

1

Local development toolkits

Teams commonly place a database GUI, an API documentation viewer, or a local mail-catching tool behind a debug or tools profile, so a plain docker compose up stays fast and lean by default.

2

Integration testing pipelines

CI configurations frequently activate a testing profile that brings up mock third-party service stand-ins and test-data seeders, keeping those same services entirely out of the way during normal local development.

3

Optional observability stacks

Projects that bundle metrics dashboards and log aggregation tools alongside the application often gate that entire monitoring stack behind a single profile, since not every developer needs full observability tooling running locally.

4

Multi-service open-source projects

Larger open-source projects that ship a full Compose-based demo environment use profiles to let new contributors choose between a minimal quick-start stack and a complete stack including every optional integration, from one unmodified file.

JFrequently Asked Questions

Q1Do services need a profile to be started by Compose?
No. A service with no profiles entry is treated as core and always starts. Profiles only matter for services that are explicitly opted into optional activation.
Q2Can a single service belong to more than one profile?
Yes. A service’s profiles list can include several names, and it will start whenever any one of those profiles is activated, not only when all of them are activated together.
Q3Will a profiled service ever start if its profile was not activated?
It can, if a core or otherwise-active service explicitly declares a dependency on it. The dependency relationship generally takes priority over profile filtering for that specific link, which is why profiles should not be relied on as a strict guarantee.
Q4What is the difference between the –profile flag and COMPOSE_PROFILES?
They activate profiles the same way; the difference is only where the choice is declared. The flag is set per command, while the environment variable sets a standing default for every Compose command run in that shell or pipeline until changed.
Q5Are profiles a way to hide sensitive services from people reading the Compose file?
No. Every service and its full configuration remain visible in the file to anyone who can read it. Profiles only change whether Compose starts that service for a given run, not whether its definition is visible.
Q6Can multiple profiles be activated at the same time?
Yes. Activating several profiles at once simply unions their services together, so a single command can bring up, for example, the core stack, the debugging tools, and the testing helpers all at once.

KSummary and Key Takeaways

The essentials, in one place

  • Unlabeled services are core and always start. Profiles only affect services explicitly tagged with a profile name.
  • Activation is explicit, through the –profile flag or the COMPOSE_PROFILES environment variable, and is fully visible at the point a command is run.
  • One Compose file can serve many situations — local development, CI, full demos — without duplicated files or manual editing.
  • Dependencies can override profile filtering. A profiled service can still start if something active genuinely depends on it.
  • Profiles are not a security boundary. They control startup, not runtime access or permissions.
  • Group related optional services under shared, purpose-named profiles rather than one profile per individual service.
  • Multiple profiles combine additively, letting one command activate exactly the mix of optional services a given situation calls for.