← Back to DevBytes

Docker Compose Profiles: Best Practices and Common Pitfalls

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:

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles