Managing complex multi-service applications during development is a pain point every DevOps engineer and backend developer knows well. You spin up a database, a cache layer, a message queue, maybe a monitoring stack — and suddenly your docker-compose up launches 12 containers when you only needed three. Docker Compose profiles, introduced in Compose v2 and now mature in 2026, solve this elegantly by letting you selectively activate services based on the task at hand.
What Are Docker Compose Profiles?
Profiles let you tag services in your compose.yaml so they only start when explicitly requested. Services without a profile always start. Services with a profile only start when that profile is activated via --profile or the COMPOSE_PROFILES environment variable.
Think of it like feature flags for your infrastructure — you define everything in one file but activate only what you need.
Basic Setup: A Real-World Example
Let’s say you’re building a SaaS app with the following services:
- API server (always needed)
- PostgreSQL (always needed)
- Redis (always needed)
- Celery worker (needed for async tasks)
- Mailhog (only for email testing)
- Prometheus + Grafana (only for monitoring/debugging)
- pgAdmin (only for database debugging)
Here’s how you structure your compose.yaml:
services:
api:
build: .
ports:
- "8000:8000"
depends_on:
- postgres
- redis
environment:
- DATABASE_URL=postgresql://app:secret@postgres:5432/appdb
- REDIS_URL=redis://redis:6379/0
postgres:
image: postgres:17
environment:
POSTGRES_DB: appdb
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
worker:
build: .
command: celery -A app worker -l info
profiles: ["worker"]
depends_on:
- postgres
- redis
mailhog:
image: mailhog/mailhog
ports:
- "1025:1025"
- "8025:8025"
profiles: ["mail"]
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
profiles: ["monitoring"]
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
depends_on:
- prometheus
profiles: ["monitoring"]
pgadmin:
image: dpage/pgadmin4
environment:
PGADMIN_DEFAULT_EMAIL: admin@local.dev
PGADMIN_DEFAULT_PASSWORD: admin
ports:
- "5050:80"
profiles: ["debug"]
volumes:
pgdata:Using Profiles in Practice
Start Only Core Services
docker compose upThis starts only api, postgres, and redis — the services without profiles. Fast and lightweight.
Add the Background Worker
docker compose --profile worker upNow you get the core services plus the Celery worker.
Activate Multiple Profiles
docker compose --profile worker --profile monitoring upThis starts core + worker + Prometheus + Grafana. Note that Grafana’s depends_on: prometheus is automatically handled.
Use Environment Variables
Instead of passing flags every time, set the COMPOSE_PROFILES variable:
# In your .env file or shell
COMPOSE_PROFILES=worker,mail
# Now just run:
docker compose upAdvanced Pattern: Per-Developer Profiles with .env
Here’s a pattern I love for teams. Create a .env.example that developers copy:
# .env.example — copy to .env and uncomment what you need
# COMPOSE_PROFILES=worker
# COMPOSE_PROFILES=worker,monitoring
# COMPOSE_PROFILES=worker,mail,debugEach developer activates only what they need. The frontend dev doesn’t load monitoring. The backend dev doesn’t load pgAdmin. Everyone’s laptop stays cool.
Profile-Aware Commands
Profiles also affect other Compose commands:
# Stop only monitoring containers
docker compose --profile monitoring stop
# View logs for worker profile services
docker compose --profile worker logs -f
# Rebuild only debug-profile services
docker compose --profile debug buildOne gotcha: docker compose down without a profile flag will not remove profiled containers that are running. Use docker compose --profile '*' down to tear down everything:
# Nuclear option: stop and remove ALL services
docker compose --profile '*' down -vCombining Profiles with Compose Watch
In 2026, docker compose watch is the standard for hot-reload during development. Profiles work seamlessly with it:
services:
api:
build: .
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: ./requirements.txt
worker:
build: .
profiles: ["worker"]
develop:
watch:
- action: sync
path: ./src
target: /app/src# Hot-reload both API and worker
docker compose --profile worker watchProfile Naming Conventions
Adopt a consistent naming scheme across projects:
worker— background job processorsmonitoring— Prometheus, Grafana, Jaegerdebug— pgAdmin, Redis Commander, debug toolsmail— Mailhog, mail testingtest— test databases, mock servicesfull— everything (assign to all optional services as a second profile)
The full profile trick is especially useful:
worker:
profiles: ["worker", "full"]
mailhog:
profiles: ["mail", "full"]
prometheus:
profiles: ["monitoring", "full"]# Start absolutely everything
docker compose --profile full upCommon Mistakes to Avoid
- Profiling your database: Don’t put core dependencies behind profiles. If your API needs Postgres, Postgres should always start.
- Forgetting depends_on chains: If service A (no profile) depends on service B (has profile), Compose will error when B isn’t activated. Keep dependency chains profile-aware.
- Overusing profiles: If you have 15 profiles, you’ve recreated the complexity you were trying to avoid. Keep it under 5-6.
Wrapping Up
Docker Compose profiles transform unwieldy multi-service setups into something manageable. You get one canonical compose.yaml that describes your entire stack, but each developer — or each task — only activates the slice they need. Combined with Compose Watch for hot-reload and environment-based activation, profiles are a must-know feature for any DevOps workflow in 2026.
Start by auditing your current compose.yaml — identify services that aren’t always needed, tag them with profiles, and watch your docker compose up time drop dramatically.

Leave a Reply