Understanding Docker Compose Profiles
Docker Compose profiles, introduced in version 1.28.0, provide a powerful mechanism for conditionally including services in your Compose configuration. A profile is essentially a tag or label assigned to a service that determines whether that service should be started when you run docker compose up. By default, only services without a profile (or with the default profile) are activated. Services tagged with a specific profile remain dormant until you explicitly activate that profile via the --profile flag or the COMPOSE_PROFILES environment variable.
Think of profiles as a way to bundle related, optional services together under a meaningful name. For example, you might have a "debug" profile for debugging tools, a "testing" profile for test databases, or a "production" profile for monitoring services. Profiles let you maintain a single, comprehensive docker-compose.yml file while selectively enabling only the services you need for a given context.
Why Profiles Matter
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Before profiles existed, developers often resorted to maintaining multiple Compose files (docker-compose.yml, docker-compose.dev.yml, docker-compose.prod.yml) and merging them with the -f flag. This approach led to configuration drift, duplication, and complexity. Profiles solve these problems elegantly:
- Single source of truth — All service definitions live in one file, eliminating duplication and reducing the risk of inconsistencies across environment-specific overrides.
- Selective resource usage — On a development machine, you can spin up only the core application and database, skipping resource-heavy analytics or batch processing services that aren't needed for local debugging.
- Environment-specific compositions — The same Compose file can serve development, CI, staging, and production simply by activating different profile combinations.
- Cleaner developer experience — New team members don't need to understand which combination of Compose files to merge; they simply activate the relevant profile.
How to Use Profiles
Defining Profiles in Your Compose File
Assigning a profile to a service is straightforward. Under the service definition, add the profiles key with a list of profile names. A service can belong to multiple profiles, and a service with no profiles key is always started by default.
version: "3.9"
services:
# Core service — always starts (no profiles key)
web:
image: nginx:alpine
ports:
- "8080:80"
# Always starts — explicitly assigned to the default profile
api:
image: my-api:latest
profiles:
- default
# Only starts when the "debug" profile is activated
debug-tools:
image: debug-tools:latest
profiles:
- debug
# Starts when either "debug" or "testing" profile is activated
mock-server:
image: mock-server:latest
profiles:
- debug
- testing
# Starts only when the "production" profile is activated
monitoring:
image: prometheus:latest
profiles:
- production
ports:
- "9090:9090"
Activating Profiles at Runtime
There are two ways to activate profiles when running docker compose up:
1. Using the --profile flag (can be repeated for multiple profiles):
# Start core services plus debug tools
docker compose --profile debug up
# Activate multiple profiles
docker compose --profile debug --profile testing up
# Short form works too
docker compose --profile debug,testing up
2. Using the COMPOSE_PROFILES environment variable:
# Set the environment variable
export COMPOSE_PROFILES=debug,testing
# Then run normally — profiles are read from the environment
docker compose up
# Or inline for a single command
COMPOSE_PROFILES=production docker compose up
When both the flag and the environment variable are used, the flag takes precedence and overrides the environment variable entirely.
Understanding Profile Inheritance and Dependencies
Profiles do not cascade through depends_on. If a profiled service depends on another service, that dependency must either have no profile (always starts) or share at least one of the same profiles, otherwise Docker Compose will fail to resolve the dependency. Consider this example:
services:
database:
image: postgres:15
# No profile — always available
data-seeder:
image: data-seeder:latest
profiles:
- seeding
depends_on:
- database # This works because database has no profile
analytics:
image: analytics:latest
profiles:
- production
depends_on:
- redis-cache # This will fail if production profile is active
# but redis-cache requires the "cache" profile
redis-cache:
image: redis:7
profiles:
- cache
In the scenario above, activating the production profile would cause an error because analytics depends on redis-cache, which requires the cache profile. You would need to activate both profiles (--profile production,cache) for the configuration to work. Alternatively, you could add production to the redis-cache service's profiles list.
Best Practices
1. Keep Core Infrastructure Profile-Free
Services that are essential to your application's basic functionality — databases, message brokers, the main API server — should not have profiles. They should start by default. Profiles are best suited for optional, auxiliary, or environment-specific services.
services:
# Good: core services always start
postgres:
image: postgres:15
redis:
image: redis:7
app:
image: my-app:latest
# Good: optional tooling uses profiles
pgadmin:
image: dpage/pgadmin4
profiles:
- admin
2. Use Descriptive, Lowercase Profile Names
Adopt a consistent naming convention. Use lowercase, hyphen-separated names that clearly communicate intent: debug, testing, production, ci, admin, logging, batch-processing. Avoid ambiguous names like mode1 or extra that future maintainers won't understand.
3. Document Profiles in Your Compose File
Use YAML comments to document which profiles exist, what they enable, and how they're intended to be used. This is invaluable for onboarding and for your future self.
# PROFILE REFERENCE
# default (no flag): core application + database + redis
# debug: adds debug-tools, mock-auth-server
# testing: adds test-database, mock-services, coverage-reporter
# production: adds monitoring, log-shipping, backup-scheduler
# admin: adds pgadmin, redis-insight
services:
# ... service definitions with profiles as documented above
4. Combine Profiles with Environment Variables for Flexibility
Use COMPOSE_PROFILES in conjunction with environment-specific .env files to create context-aware startup scripts. For example, you can create a .env.ci file that sets both COMPOSE_PROFILES and other configuration variables:
# .env.ci
COMPOSE_PROFILES=ci,testing
DB_HOST=test-db
LOG_LEVEL=debug
Then run:
docker compose --env-file .env.ci up
5. Avoid Overlapping Profile Logic
Keep profile responsibilities distinct. If you find yourself activating three or more profiles for a common workflow, consider consolidating them into a single, purpose-named profile. For instance, instead of always running --profile debug --profile mock --profile tracing together, create a single development profile that encompasses all those services.
services:
debug-tools:
profiles:
- development # consolidated from debug, mock, tracing
mock-server:
profiles:
- development
tracing-collector:
profiles:
- development
6. Test Profile Combinations
Add a simple validation step to your CI pipeline or a Makefile target that verifies your Compose file parses correctly with each profile combination you expect to use:
# Makefile target to validate all profile combinations
validate-profiles:
docker compose config > /dev/null
docker compose --profile debug config > /dev/null
docker compose --profile testing config > /dev/null
docker compose --profile production config > /dev/null
docker compose --profile debug,testing config > /dev/null
Common Pitfalls
Pitfall 1: Assuming Profiles Cascade Through Dependencies
One of the most frequent misunderstandings is expecting that activating a profile for a service will automatically pull in its dependencies even if those dependencies have different profiles. As demonstrated earlier, profile activation is flat — each service is evaluated independently. If service A (profile: "app") depends on service B (profile: "db"), activating only "app" will cause a failure because B won't be started. The solution is to either remove the profile from the dependency or ensure both profiles are activated together.
# Problematic: activating "app" won't start "db"
services:
db:
profiles:
- database
app:
profiles:
- app
depends_on:
- db
# Fixed: either remove profile from db or consolidate profiles
services:
db:
# No profile — always available
app:
profiles:
- app
depends_on:
- db
Pitfall 2: Silent Service Omission in CI/CD
In CI/CD pipelines, a missing --profile flag can lead to subtle failures. A service required for integration tests might be silently skipped, causing tests to pass (because they can't find the service and skip themselves) rather than fail loudly. Always explicitly specify profiles in CI scripts and consider adding a startup verification step that checks for expected running services.
# CI script example — always explicit
docker compose --profile ci --profile testing up -d
# Verify expected services are actually running
if ! docker compose ps --services | grep -q "integration-test-runner"; then
echo "ERROR: integration-test-runner not started — check profiles"
exit 1
fi
Pitfall 3: The "default" Profile Confusion
The default profile is implicitly active unless you override it. However, explicitly assigning a service to the default profile can behave counterintuitively when combined with the --profile flag. When you specify any profile via --profile, only the explicitly named profiles are active — the default profile is replaced, not augmented. This means services tagged only with default will not start when you pass --profile debug.
services:
always-on:
# No profiles key — truly always starts
default-only:
profiles:
- default # Starts without --profile, but NOT with --profile debug
debug-tool:
profiles:
- debug # Starts only with --profile debug
To avoid confusion, prefer omitting the profiles key entirely for services that should always start, rather than explicitly using default.
Pitfall 4: Forgetting That Profiles Affect docker compose down and Other Commands
Profiles influence more than just up. Commands like docker compose ps, docker compose logs, docker compose down, and docker compose restart also respect profiles. If you start services with a profile and then run docker compose down without the profile flag, the profiled services won't be targeted for shutdown — they'll keep running. Always match profile flags across paired up and down commands, or use docker compose down --profile ... to ensure clean teardown.
# Started with profile
docker compose --profile debug up -d
# This will NOT stop the debug services — profiles must match
docker compose down
# Correct way — include the same profiles
docker compose --profile debug down
Pitfall 5: Incompatibility with extends (Legacy)
If you're using the legacy extends field (distinct from docker compose -f multiple-file merging), be aware that profile inheritance across extended services can produce unexpected results. The extends field is deprecated in favor of using multiple -f flags or include. When migrating, ensure profiles are defined in the final, merged service definition rather than relying on extension behavior.
Conclusion
Docker Compose profiles are a deceptively simple feature with profound implications for how you manage multi-service applications across different environments. By centralizing service definitions in a single Compose file and using profiles to toggle optional components, you eliminate configuration duplication, reduce cognitive overhead, and make your development workflows more explicit and reproducible. The key to success lies in thoughtful profile design: keep core services profile-free, use clear naming conventions, document your profiles, and always be mindful of how profiles interact with dependencies and CI/CD automation. When used correctly, profiles transform your Compose file from a static blueprint into a flexible, context-aware orchestration tool that adapts gracefully to development, testing, and production scenarios alike.