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
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.
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
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.
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.
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.
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.
The always-on baseline
Any service with no profiles entry, which Compose starts regardless of which profiles, if any, were activated.
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
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.
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
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
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.
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
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
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-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-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
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.
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.
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.
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.
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.
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
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.
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.
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.
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
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.