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:
version: '3.8'
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:
version: '3.8'
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:
version: '3.8'
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:
version: '3.8'
# 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
version: '3.8'
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.