Introduction to Docker Compose Profiles
Docker Compose profiles, introduced in Docker Compose v1.28 (file format v3.9+), allow you to selectively enable or disable services based on named configuration profiles. This feature transforms how teams manage multi-environment deployments by letting a single docker-compose.yml file define services for development, testing, staging, and productionâall controlled through simple profile activation flags.
What Are Docker Compose Profiles?
A profile is a label assigned to a service in your Compose file. When a profile is active, all services tagged with that profile start alongside services that have no profile (the default profile). When a profile is inactive, those services are completely ignoredâthey are not created, started, or even evaluated for dependencies. Think of profiles as conditional gates: a service with profile production only exists when you explicitly activate the production profile.
The core syntax lives under the profiles key within a service definition:
services:
app:
image: my-app:latest
profiles:
- production
ports:
- "8080:8080"
Services without a profiles key always run regardless of active profiles. This creates a base layer of always-on infrastructure (like networks, volumes, or shared databases) that profile-specific services can depend on.
Why Profiles Matter in Production
In production environments, profiles solve several critical problems that previously required fragile workarounds like multiple Compose files, environment variable gymnastics, or complex shell scripts:
Single Source of Truth. A single docker-compose.yml defines all services across all environments. This eliminates drift between development and production configurationsâthe same file that works on a developer laptop powers the production cluster, just with different profile flags.
Explicit Intent. Profiles make environment-specific services explicit and documented. A new team member can read the Compose file and immediately understand which services belong to production, which are debugging tools, and which are shared infrastructure.
Reduced Attack Surface. In production, you can omit development-only services entirely. Debug containers, hot-reload proxies, test databases with exposed ports, and admin dashboards simply do not exist when their profiles are not activated. This is not just a configuration toggleâit is a structural security boundary because the containers are never created.
Cleaner Orchestration. Infrastructure-as-code tools, CI/CD pipelines, and container orchestrators can activate exactly the profiles needed without parsing complex merge logic. A single --profile flag replaces entire directories of environment-specific Compose file overrides.
How to Use Profiles
Profiles are activated through the --profile CLI flag or the COMPOSE_PROFILES environment variable. You can activate multiple profiles simultaneously, and all services matching any active profile will start.
CLI Activation
# Start only services with no profile (default behavior)
docker compose up -d
# Start default services plus all 'production' profile services
docker compose --profile production up -d
# Activate multiple profiles
docker compose --profile production --profile monitoring up -d
# The --profile flag works with all lifecycle commands
docker compose --profile production logs -f
docker compose --profile production ps
docker compose --profile production down
Environment Variable Activation
For CI/CD pipelines or shell scripts, set the COMPOSE_PROFILES variable to avoid repeating flags:
# Set profiles via environment variable
export COMPOSE_PROFILES=production,monitoring
docker compose up -d # automatically uses both profiles
# Useful in CI/CD scripts
COMPOSE_PROFILES=production docker compose up -d
When both the environment variable and CLI flags are present, they mergeâboth sets of profiles become active.
Complete Production Compose File Example
Below is a realistic production configuration for a web application stack. Notice how profiles separate concerns while keeping everything in one file:
version: "3.9"
services:
# ---- Always-on infrastructure (no profile) ----
reverse-proxy:
image: nginx:alpine
volumes:
- ./nginx-prod.conf:/etc/nginx/nginx.conf:ro
ports:
- "443:443"
depends_on:
- app
networks:
- frontend
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
networks:
- backend
restart: unless-stopped
# ---- Production profile services ----
app:
image: my-app:${APP_VERSION:-latest}
profiles:
- production
environment:
- REDIS_URL=redis://redis:6379
- LOG_LEVEL=warn
- WORKER_COUNT=4
depends_on:
- redis
networks:
- backend
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2'
memory: 2048M
worker:
image: my-worker:${WORKER_VERSION:-latest}
profiles:
- production
environment:
- REDIS_URL=redis://redis:6379
- QUEUE_NAME=high-priority
depends_on:
- redis
networks:
- backend
restart: unless-stopped
deploy:
resources:
limits:
cpus: '4'
memory: 4096M
# ---- Monitoring profile services ----
prometheus:
image: prom/prometheus:latest
profiles:
- monitoring
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
ports:
- "9090:9090"
networks:
- backend
restart: unless-stopped
grafana:
image: grafana/grafana:latest
profiles:
- monitoring
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
volumes:
- grafana-data:/var/lib/grafana
ports:
- "3000:3000"
depends_on:
- prometheus
networks:
- backend
restart: unless-stopped
# ---- Debug profile (development only, never in prod) ----
debug-db-browser:
image: adminer:latest
profiles:
- debug
ports:
- "8080:8080"
networks:
- backend
volumes:
redis-data:
prometheus-data:
grafana-data:
networks:
frontend:
backend:
With this single file, you can run the production stack, the monitoring layer, or both together, while debug tools remain completely absent unless explicitly requested:
# Production deployment
docker compose --profile production up -d
# Production with monitoring
docker compose --profile production --profile monitoring up -d
# Development with debug tools
docker compose --profile debug up -d
# All at once (rare, but valid)
docker compose --profile production --profile monitoring --profile debug up -d
Advanced: Profile-Conditional Dependencies
Docker Compose resolves dependencies across profiles intelligently. If service A depends on service B, and B has a profile, A must also have that same profile (or a compatible one) for the dependency to resolve. If A has no profile and B has profile production, Compose will refuse to start because the dependency cannot be satisfied when profiles are inactive.
This rule prevents broken partial startups. Consider this correct example:
services:
database:
image: postgres:15
profiles:
- production
- staging
api:
image: my-api:latest
profiles:
- production
- staging
depends_on:
- database
environment:
- DATABASE_URL=postgres://database:5432/mydb
Both services share the same profiles, so the dependency always resolves. If api had no profile while database had production, Compose would error out when starting without the profile because api would try to depend on a non-existent service.
Profile Inheritance and Service Extension
When using YAML anchors or the extends directive (deprecated in v3 but still functional), profiles can be inherited or overridden:
services:
base-app: &base-app
image: my-app:latest
environment:
- COMMON_ENV=value
app-production:
<<: *base-app
profiles:
- production
environment:
- LOG_LEVEL=warn
deploy:
resources:
limits:
cpus: '4'
app-staging:
<<: *base-app
profiles:
- staging
environment:
- LOG_LEVEL=debug
deploy:
resources:
limits:
cpus: '1'
This approach keeps configuration DRY while maintaining profile-specific overrides.
Best Practices for Production Profiles
1. Keep Always-On Services Minimal. Services without profiles form the shared backbone. Limit these to network definitions, persistent storage volumes, and truly universal dependencies like a reverse proxy or shared message broker. Every additional profile-less service increases production attack surface and resource consumption unnecessarily.
2. Use Descriptive Profile Names. Choose names that communicate intent: production, staging, monitoring, debug, batch-processing. Avoid cryptic abbreviations or environment codes that new team members must decipher. Profiles serve as documentationâmake them readable.
3. Never Mix Production and Debug Profiles. Create a strict policy: the debug profile (or any profile exposing insecure endpoints) must never be activated in production. You can enforce this in CI/CD by validating that COMPOSE_PROFILES does not contain debug when deploying to production environments.
4. Pair Profiles with Environment Variables. Use ${VAR} substitution for environment-specific values, then set those variables per deployment context. Profiles control which services exist; environment variables control how they behave. Together they provide complete environment polymorphism:
services:
app:
image: my-app:${APP_VERSION}
profiles:
- production
environment:
- LOG_LEVEL=${LOG_LEVEL:-info}
- DB_MAX_CONNECTIONS=${DB_MAX_CONNECTIONS:-50}
5. Document Profile Combinations. Maintain a comment block at the top of your Compose file explaining common profile combinations and their use cases:
# Profile combinations:
# --profile production : Core production stack
# --profile production --profile monitoring : Production with observability
# --profile debug : Local development with debug tools
# WARNING: Never activate 'debug' profile in production
# WARNING: 'monitoring' profile exposes ports 9090 and 3000
6. Validate Profiles in CI/CD. Add a pre-deployment check that inspects the Compose file and confirms only allowed profiles are active for the target environment. A simple script can parse the Compose YAML and cross-reference with allowed lists:
# CI/CD validation script snippet
ALLOWED_PROFILES="production,monitoring"
ACTIVE_PROFILES="${COMPOSE_PROFILES}"
for profile in $(echo "$ACTIVE_PROFILES" | tr ',' ' '); do
if ! echo "$ALLOWED_PROFILES" | grep -qw "$profile"; then
echo "ERROR: Profile '$profile' is not allowed in production"
exit 1
fi
done
7. Test Profile Isolation. Regularly verify that deactivating non-production profiles truly removes those services. Run docker compose ps with and without profiles to confirm no debug containers leak into production. This should be part of your staging deployment pipeline.
8. Combine with Resource Limits. Production profile services should declare explicit resource constraints using the deploy.resources directive. This ensures production containers cannot consume all host resources, while development profile services might intentionally omit limits for flexibility:
services:
app-production:
profiles:
- production
deploy:
resources:
limits:
cpus: '4'
memory: 4096M
reservations:
cpus: '2'
memory: 2048M
app-development:
profiles:
- debug
# No resource limits during development for faster iteration
Conclusion
Docker Compose profiles transform environment management from a multi-file headache into a clean, declarative, single-file solution. By tagging services with named profiles and activating them selectively via CLI flags or environment variables, you gain precise control over which containers exist in each deployment context. In production, this means a smaller attack surface, clearer configuration intent, and simpler CI/CD integration. Adopt profiles early in your project lifecycle, document your profile combinations clearly, and enforce profile policies in your deployment pipeline. With these practices, your Compose files will scale gracefully from a single developer's laptop to a full production clusterâall from the same source of truth.