$docker.recipes
·9 min read·Updated February 2026

Docker Compose Profiles: Managing Optional Services Without Multiple Files

Stop maintaining separate docker-compose.override.yml files. Profiles let you group optional services and start only what you need with a single flag.

docker-composeprofilesdevelopmentdevops

01The Problem With Always-Running Services

Every Docker Compose stack accumulates optional services over time. Debug tools like pgAdmin, Mailpit, or Adminer. Monitoring agents you only need when troubleshooting. Seed containers that load test data. One-off migration scripts. The traditional approach is separate files: docker-compose.yml for production, docker-compose.override.yml for development, docker-compose.debug.yml for debugging. You end up with docker compose -f docker-compose.yml -f docker-compose.debug.yml -f docker-compose.monitoring.yml up and nobody can remember the right combination of flags. Docker Compose profiles solve this elegantly. You define profiles on individual services, and those services only start when their profile is explicitly activated. Everything in one file, selectively started based on what you need right now. I've migrated all my stacks to use profiles, and the cognitive load dropped significantly. My team stopped asking "which compose files do I need?" and started running docker compose --profile debug up instead.

02How Profiles Work

Services without a profile always start. Services with a profile only start when that profile is activated. It's that simple.
[docker-compose.yml]
1services:
2 # No profile — always starts
3 app:
4 image: myapp:latest
5 ports:
6 - "8080:8080"
7
8 # No profile — always starts
9 db:
10 image: postgres:16-alpine
11 volumes:
12 - pgdata:/var/lib/postgresql/data
13
14 # Only starts with --profile debug
15 pgadmin:
16 image: dpage/pgadmin4:latest
17 profiles: [debug]
18 ports:
19 - "5050:80"
20 environment:
21 - PGADMIN_DEFAULT_EMAIL=admin@local.dev
22 - PGADMIN_DEFAULT_PASSWORD=admin
23
24 # Only starts with --profile monitoring
25 prometheus:
26 image: prom/prometheus:latest
27 profiles: [monitoring]
28 ports:
29 - "9090:9090"
30
31 # Multiple profiles — starts with either
32 grafana:
33 image: grafana/grafana:latest
34 profiles: [monitoring, debug]
35 ports:
36 - "3000:3000"

A service can belong to multiple profiles. In the example above, Grafana starts if you activate either the monitoring or debug profile. Use docker compose --profile debug --profile monitoring up to activate multiple profiles at once.

03Practical Profile Patterns

Here are the profile groupings I use across my stacks. These patterns have evolved over time and cover most real-world scenarios.
[docker-compose.yml]
1services:
2 app:
3 image: myapp:latest
4 ports:
5 - "8080:8080"
6 depends_on:
7 db:
8 condition: service_healthy
9
10 db:
11 image: postgres:16-alpine
12 healthcheck:
13 test: ["CMD", "pg_isready"]
14 interval: 5s
15 timeout: 3s
16 retries: 5
17
18 # --- Debug Profile ---
19 pgadmin:
20 profiles: [debug]
21 image: dpage/pgadmin4:latest
22 ports:
23 - "5050:80"
24
25 mailpit:
26 profiles: [debug]
27 image: axllent/mailpit:latest
28 ports:
29 - "8025:8025" # Web UI
30 - "1025:1025" # SMTP
31
32 # --- Monitoring Profile ---
33 prometheus:
34 profiles: [monitoring]
35 image: prom/prometheus:latest
36
37 node-exporter:
38 profiles: [monitoring]
39 image: prom/node-exporter:latest
40
41 # --- Seed Profile (run once, then exit) ---
42 seed:
43 profiles: [seed]
44 image: myapp:latest
45 command: ["npm", "run", "seed"]
46 depends_on:
47 db:
48 condition: service_healthy

04Using COMPOSE_PROFILES in .env

Instead of typing --profile flags every time, set the COMPOSE_PROFILES environment variable. This is especially useful for development environments where you always want certain profiles active.
[.env]
1# .env file — these profiles activate automatically
2# Comma-separated list, no spaces
3COMPOSE_PROFILES=debug,monitoring
4
5# Now just run:
6# docker compose up -d
7# Equivalent to: docker compose --profile debug --profile monitoring up -d

Services without any profile always start, regardless of which profiles are active. This is intentional — your core services (app, database, cache) should have no profile so they always run. Only add profiles to genuinely optional services. A common mistake is adding profiles to everything, which means docker compose up with no profiles starts nothing.

05Best Practices and Gotchas

After using profiles across a dozen stacks, here are the lessons learned: Naming convention: Use short, descriptive profile names. I standardize on: debug (development tools), monitoring (observability), seed (data seeding), test (test dependencies), and admin (administrative UIs). Consistent naming across projects means less documentation. depends_on with profiles: A core service cannot depend on a profiled service. If your app depends_on a service that has a profile, the app won't start unless that profile is active. Keep depends_on relationships within the same profile group or pointing at unprofiled services only. docker compose down respects profiles: If you started with --profile debug, running docker compose down without the profile flag will leave the debug containers running. Always use the same profile flags for down as you used for up, or use docker compose down --remove-orphans to clean up everything. Profiles and extends: If you're using the extends keyword, the profile from the extended service is inherited. Be aware of this when building shared service definitions. One thing I wish profiles supported: per-profile environment variables. Currently, you can't change an environment variable based on which profile is active. For that, you still need separate .env files or docker-compose.override.yml. It's the one remaining case where I use multiple Compose files.

About the Author

Frank Pegasus

DevOps engineer and self-hosting enthusiast with over a decade of experience running containerized workloads in production. Creator of docker.recipes.