You’ve got a Docker Compose setup with 15 services: databases, caches, message queues, debugging tools, and your app. In development, you need everything. In production, you need four. You could maintain two separate compose files, but that’s friction. Profiles solve this elegantly.
Profiles let you tag services and conditionally activate them at runtime. One compose file, multiple configurations.
The Problem They Solve
Imagine this setup:
services: postgres: image: postgres:15 environment: POSTGRES_PASSWORD: dev
redis: image: redis:7 # Used in production
adminer: image: adminer # Database GUI, dev-only ports: - "8080:8080"
jaeger: image: jaegertracing/all-in-one # Distributed tracing, dev-only ports: - "6831:6831/udp" - "16686:16686"
prometheus: image: prom/prometheus # Metrics, dev-only ports: - "9090:9090"
app: build: . depends_on: - postgres - redisWhen you run docker compose up, all 6 services start. In production, you only want postgres, redis, and app. The dev tools (adminer, jaeger, prometheus) are noise.
You could delete those services or maintain separate files:
docker-compose.yml (dev + prod)docker-compose.prod.yml (prod only)But every change to the base config means updating two files. Profiles are cleaner.
How Profiles Work
Assign services to a profile. Specify which profiles to activate:
services: postgres: image: postgres:15 environment: POSTGRES_PASSWORD: dev
redis: image: redis:7
# Dev-only tools adminer: image: adminer profiles: - debug ports: - "8080:8080"
jaeger: image: jaegertracing/all-in-one profiles: - debug ports: - "6831:6831/udp" - "16686:16686"
prometheus: image: prom/prometheus profiles: - debug ports: - "9090:9090"
app: build: . depends_on: - postgres - redisNow:
# Start only postgres, redis, app$ docker compose upCreating network ...Creating postgres ...Creating redis ...Creating app ...adminer, jaeger, prometheus are skipped
# Start with debug tools$ docker compose --profile debug upCreating network ...Creating postgres ...Creating redis ...Creating adminer ...Creating jaeger ...Creating prometheus ...Creating app ...
# Activate multiple profiles$ docker compose --profile debug --profile testing upServices without a profile always start. Services with a profile only start if that profile is activated.
Practical Dev/Prod Pattern
Here’s a real-world example: separate dev, testing, and production profiles:
services: # Always-on postgres: image: postgres:15 environment: POSTGRES_PASSWORD: secret POSTGRES_DB: mydb
redis: image: redis:7
app: build: . environment: DATABASE_URL: postgresql://postgres:secret@postgres/mydb REDIS_URL: redis://redis:6379
# Development tools (dev profile) adminer: image: adminer profiles: - dev ports: - "8080:8080" depends_on: - postgres
pgweb: image: sosedoff/pgweb profiles: - dev ports: - "8081:8081" depends_on: - postgres environment: DATABASE_URL: postgresql://postgres:secret@postgres/mydb?sslmode=disable
# Testing tools (test profile) postgres-test: image: postgres:15 profiles: - test environment: POSTGRES_PASSWORD: test POSTGRES_DB: test_db tmpfs: - /var/lib/postgresql/data
# Performance testing (perf profile) locust: image: locustio/locust profiles: - perf ports: - "8089:8089" volumes: - ./loadtest:/home/locustNow you can:
# Minimal: just app + core services$ docker compose up
# Development: add debugging and admin tools$ docker compose --profile dev up
# Testing: add test database$ docker compose --profile test up
# Performance testing: add load generator$ docker compose --profile perf up
# All of it$ docker compose --profile dev --profile test --profile perf upSet Default Profiles in Compose
You can set a default profile in the compose file so certain profiles always activate:
# Activate 'dev' by default in developmentservices: app: build: . # ...
adminer: image: adminer profiles: - dev # ...Then create a .env file (or export):
COMPOSE_PROFILES=dev$ export COMPOSE_PROFILES=dev$ docker compose up # Activates dev profile by defaultOr in .env:
COMPOSE_PROFILES=dev$ docker compose up # Reads .env, activates devCommand Examples
# List all services (including those in profiles)$ docker compose config
# List active services only (current profile)$ docker compose ps
# Start a specific service in a profile$ docker compose --profile debug up adminer
# Execute command in a service within a profile$ docker compose --profile debug exec adminer bash
# View logs for a profiled service$ docker compose --profile debug logs adminer
# Stop, but keep the profile active$ docker compose --profile debug stop
# Remove everything (respects active profiles)$ docker compose --profile dev downReal-World Microservices Example
services: # Core services api: build: context: ./api environment: DB_URL: postgresql://postgres:pwd@postgres:5432/api CACHE_URL: redis://redis:6379/0
postgres: image: postgres:15 volumes: - postgres_data:/var/lib/postgresql/data
redis: image: redis:7
# Optional services workers: build: context: ./workers profiles: - workers depends_on: - redis
elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:8.0.0 profiles: - search environment: discovery.type: single-node
kibana: image: docker.elastic.co/kibana/kibana:8.0.0 profiles: - search ports: - "5601:5601"
# Development-only adminer: image: adminer profiles: - dev ports: - "8080:8080"
volumes: postgres_data:Usage:
# Production: api, postgres, redis$ docker compose up -d
# Development: add admin tools$ docker compose --profile dev up -d
# With search indexing: add elasticsearch + kibana$ docker compose --profile search up -d
# Full stack: everything$ docker compose --profile dev --profile search --profile workers up -dChecklist
- Identify services that aren’t always needed
- Add
profiles: [name]to those services - Create a profile for each group (dev, test, perf, etc.)
- Test:
docker compose --profile <name> up - Document in README which profiles exist
- Consider setting default profiles in .env
- Use profiles to reduce noise in local development
Profiles are underused. They eliminate the friction of maintaining multiple compose files and let you keep one source of truth. Your 15-service dev stack doesn’t need to run locally every time.
Common Questions
Do Docker Compose services without a profile always start?
Yes, services without a profiles key always start. Compose launches them on every docker compose up, while profiled services stay skipped until you activate their profile. In the post’s example, postgres, redis, and app always run, and adminer, jaeger, and prometheus wait for --profile debug.
How do I activate multiple Docker Compose profiles at once?
Repeat the --profile flag once per profile. The command docker compose --profile dev --profile test --profile perf up starts the always-on services plus every service tagged with any of those three profiles. Each extra flag adds one more group of services to the run.
How do I set a default Docker Compose profile?
Set the COMPOSE_PROFILES variable, for example COMPOSE_PROFILES=dev, in a .env file or with export. A plain docker compose up then activates the dev profile without a --profile flag. The variable saves you from typing the flag on every command during local development.
Do I need separate compose files for dev and prod?
No, one compose file is enough. Profiles tag optional services such as adminer or a load generator inside the same docker-compose.yml, so a base change gets made once. Separate dev and prod files force you to update two files for every change to the base config.