← Back to DevBytes

Docker Compose Profiles: Production Guide

Introduction to Docker Compose Profiles

Docker Compose profiles, introduced in Docker Compose v1.28 (file format v3.9+), allow you to selectively enable or disable services based on named configuration profiles. This feature transforms how teams manage multi-environment deployments by letting a single docker-compose.yml file define services for development, testing, staging, and production—all controlled through simple profile activation flags.

What Are Docker Compose Profiles?

A profile is a label assigned to a service in your Compose file. When a profile is active, all services tagged with that profile start alongside services that have no profile (the default profile). When a profile is inactive, those services are completely ignored—they are not created, started, or even evaluated for dependencies. Think of profiles as conditional gates: a service with profile production only exists when you explicitly activate the production profile.

The core syntax lives under the profiles key within a service definition:

services:
  app:
    image: my-app:latest
    profiles:
      - production
    ports:
      - "8080:8080"

Services without a profiles key always run regardless of active profiles. This creates a base layer of always-on infrastructure (like networks, volumes, or shared databases) that profile-specific services can depend on.

Why Profiles Matter in Production

In production environments, profiles solve several critical problems that previously required fragile workarounds like multiple Compose files, environment variable gymnastics, or complex shell scripts:

Single Source of Truth. A single docker-compose.yml defines all services across all environments. This eliminates drift between development and production configurations—the same file that works on a developer laptop powers the production cluster, just with different profile flags.

Explicit Intent. Profiles make environment-specific services explicit and documented. A new team member can read the Compose file and immediately understand which services belong to production, which are debugging tools, and which are shared infrastructure.

Reduced Attack Surface. In production, you can omit development-only services entirely. Debug containers, hot-reload proxies, test databases with exposed ports, and admin dashboards simply do not exist when their profiles are not activated. This is not just a configuration toggle—it is a structural security boundary because the containers are never created.

Cleaner Orchestration. Infrastructure-as-code tools, CI/CD pipelines, and container orchestrators can activate exactly the profiles needed without parsing complex merge logic. A single --profile flag replaces entire directories of environment-specific Compose file overrides.

How to Use Profiles

Profiles are activated through the --profile CLI flag or the COMPOSE_PROFILES environment variable. You can activate multiple profiles simultaneously, and all services matching any active profile will start.

CLI Activation

# Start only services with no profile (default behavior)
docker compose up -d

# Start default services plus all 'production' profile services
docker compose --profile production up -d

# Activate multiple profiles
docker compose --profile production --profile monitoring up -d

# The --profile flag works with all lifecycle commands
docker compose --profile production logs -f
docker compose --profile production ps
docker compose --profile production down

Environment Variable Activation

For CI/CD pipelines or shell scripts, set the COMPOSE_PROFILES variable to avoid repeating flags:

# Set profiles via environment variable
export COMPOSE_PROFILES=production,monitoring
docker compose up -d  # automatically uses both profiles

# Useful in CI/CD scripts
COMPOSE_PROFILES=production docker compose up -d

When both the environment variable and CLI flags are present, they merge—both sets of profiles become active.

Complete Production Compose File Example

Below is a realistic production configuration for a web application stack. Notice how profiles separate concerns while keeping everything in one file:

version: "3.9"

services:
  # ---- Always-on infrastructure (no profile) ----
  reverse-proxy:
    image: nginx:alpine
    volumes:
      - ./nginx-prod.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "443:443"
    depends_on:
      - app
    networks:
      - frontend

  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data
    networks:
      - backend
    restart: unless-stopped

  # ---- Production profile services ----
  app:
    image: my-app:${APP_VERSION:-latest}
    profiles:
      - production
    environment:
      - REDIS_URL=redis://redis:6379
      - LOG_LEVEL=warn
      - WORKER_COUNT=4
    depends_on:
      - redis
    networks:
      - backend
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2048M

  worker:
    image: my-worker:${WORKER_VERSION:-latest}
    profiles:
      - production
    environment:
      - REDIS_URL=redis://redis:6379
      - QUEUE_NAME=high-priority
    depends_on:
      - redis
    networks:
      - backend
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: '4'
          memory: 4096M

  # ---- Monitoring profile services ----
  prometheus:
    image: prom/prometheus:latest
    profiles:
      - monitoring
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    ports:
      - "9090:9090"
    networks:
      - backend
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    profiles:
      - monitoring
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
    volumes:
      - grafana-data:/var/lib/grafana
    ports:
      - "3000:3000"
    depends_on:
      - prometheus
    networks:
      - backend
    restart: unless-stopped

  # ---- Debug profile (development only, never in prod) ----
  debug-db-browser:
    image: adminer:latest
    profiles:
      - debug
    ports:
      - "8080:8080"
    networks:
      - backend

volumes:
  redis-data:
  prometheus-data:
  grafana-data:

networks:
  frontend:
  backend:

With this single file, you can run the production stack, the monitoring layer, or both together, while debug tools remain completely absent unless explicitly requested:

# Production deployment
docker compose --profile production up -d

# Production with monitoring
docker compose --profile production --profile monitoring up -d

# Development with debug tools
docker compose --profile debug up -d

# All at once (rare, but valid)
docker compose --profile production --profile monitoring --profile debug up -d

Advanced: Profile-Conditional Dependencies

Docker Compose resolves dependencies across profiles intelligently. If service A depends on service B, and B has a profile, A must also have that same profile (or a compatible one) for the dependency to resolve. If A has no profile and B has profile production, Compose will refuse to start because the dependency cannot be satisfied when profiles are inactive.

This rule prevents broken partial startups. Consider this correct example:

services:
  database:
    image: postgres:15
    profiles:
      - production
      - staging

  api:
    image: my-api:latest
    profiles:
      - production
      - staging
    depends_on:
      - database
    environment:
      - DATABASE_URL=postgres://database:5432/mydb

Both services share the same profiles, so the dependency always resolves. If api had no profile while database had production, Compose would error out when starting without the profile because api would try to depend on a non-existent service.

Profile Inheritance and Service Extension

When using YAML anchors or the extends directive (deprecated in v3 but still functional), profiles can be inherited or overridden:

services:
  base-app: &base-app
    image: my-app:latest
    environment:
      - COMMON_ENV=value

  app-production:
    <<: *base-app
    profiles:
      - production
    environment:
      - LOG_LEVEL=warn
    deploy:
      resources:
        limits:
          cpus: '4'

  app-staging:
    <<: *base-app
    profiles:
      - staging
    environment:
      - LOG_LEVEL=debug
    deploy:
      resources:
        limits:
          cpus: '1'

This approach keeps configuration DRY while maintaining profile-specific overrides.

Best Practices for Production Profiles

1. Keep Always-On Services Minimal. Services without profiles form the shared backbone. Limit these to network definitions, persistent storage volumes, and truly universal dependencies like a reverse proxy or shared message broker. Every additional profile-less service increases production attack surface and resource consumption unnecessarily.

2. Use Descriptive Profile Names. Choose names that communicate intent: production, staging, monitoring, debug, batch-processing. Avoid cryptic abbreviations or environment codes that new team members must decipher. Profiles serve as documentation—make them readable.

3. Never Mix Production and Debug Profiles. Create a strict policy: the debug profile (or any profile exposing insecure endpoints) must never be activated in production. You can enforce this in CI/CD by validating that COMPOSE_PROFILES does not contain debug when deploying to production environments.

4. Pair Profiles with Environment Variables. Use ${VAR} substitution for environment-specific values, then set those variables per deployment context. Profiles control which services exist; environment variables control how they behave. Together they provide complete environment polymorphism:

services:
  app:
    image: my-app:${APP_VERSION}
    profiles:
      - production
    environment:
      - LOG_LEVEL=${LOG_LEVEL:-info}
      - DB_MAX_CONNECTIONS=${DB_MAX_CONNECTIONS:-50}

5. Document Profile Combinations. Maintain a comment block at the top of your Compose file explaining common profile combinations and their use cases:

# Profile combinations:
#   --profile production             : Core production stack
#   --profile production --profile monitoring : Production with observability
#   --profile debug                  : Local development with debug tools
#   WARNING: Never activate 'debug' profile in production
#   WARNING: 'monitoring' profile exposes ports 9090 and 3000

6. Validate Profiles in CI/CD. Add a pre-deployment check that inspects the Compose file and confirms only allowed profiles are active for the target environment. A simple script can parse the Compose YAML and cross-reference with allowed lists:

# CI/CD validation script snippet
ALLOWED_PROFILES="production,monitoring"
ACTIVE_PROFILES="${COMPOSE_PROFILES}"

for profile in $(echo "$ACTIVE_PROFILES" | tr ',' ' '); do
  if ! echo "$ALLOWED_PROFILES" | grep -qw "$profile"; then
    echo "ERROR: Profile '$profile' is not allowed in production"
    exit 1
  fi
done

7. Test Profile Isolation. Regularly verify that deactivating non-production profiles truly removes those services. Run docker compose ps with and without profiles to confirm no debug containers leak into production. This should be part of your staging deployment pipeline.

8. Combine with Resource Limits. Production profile services should declare explicit resource constraints using the deploy.resources directive. This ensures production containers cannot consume all host resources, while development profile services might intentionally omit limits for flexibility:

services:
  app-production:
    profiles:
      - production
    deploy:
      resources:
        limits:
          cpus: '4'
          memory: 4096M
        reservations:
          cpus: '2'
          memory: 2048M

  app-development:
    profiles:
      - debug
    # No resource limits during development for faster iteration

Conclusion

Docker Compose profiles transform environment management from a multi-file headache into a clean, declarative, single-file solution. By tagging services with named profiles and activating them selectively via CLI flags or environment variables, you gain precise control over which containers exist in each deployment context. In production, this means a smaller attack surface, clearer configuration intent, and simpler CI/CD integration. Adopt profiles early in your project lifecycle, document your profile combinations clearly, and enforce profile policies in your deployment pipeline. With these practices, your Compose files will scale gracefully from a single developer's laptop to a full production cluster—all from the same source of truth.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles