Skip to content
Home
Advanced Docker Compose: Profiles, Extensions & Production

Advanced Docker Compose: Profiles, Extensions & Production

Docker Docker 7 min read 1466 words Beginner ExcellentWiki Editorial Team

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 services

Real-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: 512M

This 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: 128M

Limits 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: bind

Named 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: true

The 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: true

Docker 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: SIGTERM

The 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.

Section: Docker 1466 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top