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.