Advanced Docker Compose: Profiles, Extensions & Production
Docker Compose is the standard tool for defining and running multi-container applications, but most teams only use a fraction of its capabilities. Beyond basic docker compose up, Compose offers profiles for conditional services, YAML extension fields for DRY configuration, health check dependencies for graceful startup ordering, resource constraints for production stability, and isolated networking patterns. This guide explores these advanced features with production-tested patterns used by teams deploying multi-service applications at scale.
Compose Profiles for Environment Management
Profiles let you define services that start only when explicitly selected, replacing the need for multiple Compose files in many scenarios. This keeps environment-specific configuration in a single file without bloating the default startup. The official Docker Compose specification supports profiles as a first-class feature since Compose v2.
Defining and Activating Profiles
services:
api:
image: my-api:latest
ports:
- "8000:8000"
admin-tool:
image: admin:latest
profiles: ["admin"]
test-db:
image: postgres:16-alpine
profiles: ["test"]
mail-server:
image: mailhog:latest
profiles: ["dev", "test"]docker compose up -d # api only
docker compose --profile dev up -d # api + mail-server
docker compose --profile admin --profile dev up -d # api + admin + mail
docker compose --profile "*" up -d # all servicesReal-World Profile Strategy
A typical SaaS application uses three profiles:
- dev — MailHog, mock authentication service, local S3 emulator
- test — Test database, Redis for integration tests, wiremock endpoints
- admin — Database admin tool (Adminer/pgAdmin), message queue inspector, log viewer
This approach, documented in Docker’s official Compose specification, keeps the development environment lean by default while providing full infrastructure on demand. The docker compose --profile "*" up -d command is particularly useful in CI for spinning up the entire test environment. KodeKloud’s Docker Certified Associate course emphasizes profiles as the recommended pattern for managing multi-environment Compose setups.
YAML Extension Fields
Extension fields, prefixed with x-, define reusable configuration blocks that multiple services reference via YAML anchors. This eliminates duplication and enforces consistent settings across services — a pattern borrowed from enterprise YAML configuration management.
Logging and Deployment Extensions
x-logging: &logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
x-deploy: &deploy
restart: unless-stopped
healthcheck:
interval: 30s
timeout: 10s
retries: 3
services:
api:
image: my-api:latest
<<: [*logging, *deploy]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
worker:
image: my-worker:latest
<<: *logging
deploy:
<<: *deploy
resources:
limits:
memory: 512MThis pattern ensures all services share the same logging configuration and restart policy. When you need to change the log driver or retention policy, you update one block instead of every service definition. The Compose specification supports nesting anchors, as shown with the deploy block being partially overridden per service. This approach is particularly valuable in monorepos where dozens of services share common configuration patterns.
Health Check Dependencies
Compose supports condition-based service dependencies that wait for a service to be healthy before starting dependent services. This replaces the older depends_on behavior that only waited for container startup.
Chained Health Checks
services:
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 5s
retries: 5
api:
build: ./api
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
nginx:
image: nginx:alpine
depends_on:
api:
condition: service_healthy
ports:
- "80:80"This chain ensures the database is accepting connections before the API starts, and the API is responding before Nginx routes traffic to it. Without health check conditions, services start in dependency order but may race — the API could start before PostgreSQL finishes initializing its data directory. The Docker Compose documentation recommends health check dependencies for all production deployments with stateful services.
Writing Effective Health Checks
Write health checks that verify actual service readiness, not just process existence. pg_isready checks that PostgreSQL is accepting connections. curl -f http://localhost:8000/health should check database connectivity and cache warmness inside the API. A health check that returns 200 but has no database connection is worse than no health check — it creates false confidence. The start_period field is critical for services with long initialization times, as it gives the service time to warm up before health check failures count toward retries.
Resource Management in Production
Production Compose deployments must specify resource constraints to prevent a single service from starving others. Without limits, a memory leak in one container can exhaust host memory and trigger the kernel OOM killer.
CPU and Memory Limits
services:
api:
image: my-api:latest
deploy:
resources:
limits:
cpus: "0.5"
memory: 256M
reservations:
cpus: "0.25"
memory: 128MLimits set the maximum resources a container can consume. Reservations guarantee minimum resources. The Docker documentation recommends setting both values for production services — reservations ensure the container gets minimum resources, while limits prevent it from affecting other containers during traffic spikes.
Storage Configuration for Production
For production, use named volumes with explicit driver options rather than bind mounts:
volumes:
postgres_data:
driver: local
driver_opts:
type: none
device: /data/postgres
o: bindNamed volumes are managed by Docker, appear in docker volume ls, and support backup and migration workflows. Bind mounts create host filesystem dependencies that make the Compose file less portable across environments.
Production Networking Patterns
Network isolation is a security best practice that limits which services can communicate with each other. The principle of least privilege applies to container networking just as it does to firewall rules.
Frontend-Backend Isolation
services:
api:
networks:
- frontend
- backend
db:
networks:
- backend
nginx:
networks:
- frontend
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: trueThe backend network has internal: true, meaning it has no external connectivity. The database is only reachable from services on the backend network (the API). Nginx on the frontend network cannot directly connect to the database. This defense-in-depth approach limits the blast radius if a service is compromised — an attacker who gains control of Nginx cannot reach the database layer.
Secrets Management and Configuration Externalization
Docker Secrets with Compose
Use Docker secrets with Compose for sensitive data:
services:
api:
image: my-api:latest
secrets:
- db_password
- api_key
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
external: trueDocker secrets are mounted as files in /run/secrets/ inside the container. They are never exposed as environment variables, which can leak through docker inspect or logging output. The Docker secrets documentation emphasizes this file-based approach as more secure than environment variable injection.
Graceful Shutdown Configuration
Configure stop signals and grace periods for clean container shutdown:
services:
api:
image: my-api:latest
stop_grace_period: 30s
stop_signal: SIGTERMThe stop_grace_period gives the application time to finish in-flight requests, close database connections, and flush logs. If the container does not exit within the grace period, Docker sends SIGKILL. Set this long enough for your application’s maximum request processing time plus cleanup overhead.
FAQ
When should I use profiles versus separate Compose files?
Profiles are best for environment-specific services (dev tools, test databases) within the same application stack. Separate Compose files work better when the difference is in configuration values rather than service presence — combine them with docker compose -f base.yml -f production.yml. Profiles keep everything in one file, which simplifies team understanding and CI configuration.
Can extension fields reference other extension fields?
Yes. YAML anchors support composition. You can define x-base: &base with common config, then x-web: &web that includes <<: *base and adds web-specific defaults. This creates a hierarchy of configuration inheritance similar to object-oriented programming. This is documented in the YAML spec and fully supported by Docker Compose.
How do health checks affect startup time?
Health checks add startup latency because each service waits for its dependencies to be healthy. However, the total startup time is usually shorter than a fixed sleep because services start as soon as dependencies are actually ready, not after an arbitrary timeout. The start_period field lets you specify an initial grace period before health check failures count toward retries — critical for services like databases that need time for recovery or replication catch-up.
What happens when a container exceeds its memory limit?
Docker’s OOM killer terminates the container. Configure restart: unless-stopped so the container automatically restarts after OOM termination, but monitor OOM events as they indicate a memory leak or undersized limit. Use docker inspect to check OOMKilled status in the container state. Set up monitoring alerts when OOM kills occur.
Is it safe to use internal networks with databases?
Yes, and it is recommended by Docker’s security documentation. The database does not need external internet access — it only communicates with application services. An internal network prevents containers from reaching the database through the host’s external interface, adding a security layer. Apply the principle of least privilege to container networking.
Conclusion
Advanced Docker Compose features transform a simple development orchestrator into a production-ready multi-service deployment tool. Profiles manage conditional service groups, extension fields eliminate YAML duplication, health checks ensure correct startup ordering, resource constraints prevent noisy-neighbor problems, and network isolation improves security. Applying these patterns makes Compose-based deployments more reliable, maintainable, and secure. For more on container orchestration, see our guides on Docker Swarm and Kubernetes Helm Charts.