Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 31

Customizing Compose for Different Environments

Recommended resources

Hello! Welcome to your next Docker lesson.

In our last session, we covered how to securely manage secrets in Docker Compose, moving away from risky practices like hardcoding credentials and instead using .env files and, more securely, Docker's native secrets feature. This was a crucial step in preparing an application for production.

Today, we'll address another fundamental aspect of professional application deployment: managing different configurations for different environments. Your development setup, with its need for debugging and hot-reloading, is very different from a production setup, which prioritizes performance, security, and stability.

This lesson directly addresses the learning outcome: Override and extend Compose configurations for different environments (e.g., development vs. production). As a DevOps Engineer, mastering this is essential for building robust, repeatable deployment pipelines and eliminating the classic "it works on my machine" problem.

1. The Challenge: Configuration Drift

When building applications, the configuration you need for local development rarely matches what you need for production.

  • Development: You want to mount your source code for hot-reloading, expose a debugger port, use verbose logging, and maybe run services that aren't needed in production (like a local mail server).
  • Production: You need lean, optimized container images, no source code mounts, stricter security, resource limits (CPU/memory), and replication for high availability.

Trying to manage these differences by commenting and uncommenting lines in a single docker-compose.yml file is error-prone and doesn't scale. The solution is to separate your configurations in a clean, maintainable way.


2. The Core Strategy: Using Multiple Compose Files

The most common and powerful way to manage environment-specific configurations is to use multiple Compose files. The general idea is to have a base file with shared configurations and then layer environment-specific files on top of it.

Multiple Docker Compose Files for Environment-Specific Configurations
This diagram illustrates the core concept. A base `docker-compose.yml` file can be extended or overridden by environment-specific files like `docker-compose.prod.yml` or a `docker-compose.override.yml` for local development, creating a final, merged configuration.

There are two primary approaches to this.

Approach A: The Automatic Override File

For local development, Docker Compose provides a convenient convention. If you have a file named docker-compose.yml and another named docker-compose.override.yml in the same directory, running docker compose up will automatically merge them. The override.yml file's settings will be applied on top of the base file.

This is the perfect pattern for development:

  • docker-compose.yml: Contains the core service definitions that are common to all environments (e.g., service names, base images, network connections).
  • docker-compose.override.yml: Contains your development-only settings, such as bind mounts for code, debugger ports, and specific environment variables. This file is typically added to .gitignore.

Multi-Environment Deployments with Docker: A Guide

This article, 'Multi-Environment Deployments with Docker', provides a clear explanation of how the automatic override file works.

Please read the section titled 'Using Docker Compose Overrides'. It clearly demonstrates how a base docker-compose.yml can be paired with a docker-compose.override.yml for development and a separate, explicitly named docker-compose.prod.yml for production.

Approach B: Explicitly Specifying Files with -f

When you need more control, especially for staging or production environments in a CI/CD pipeline, you explicitly tell Docker Compose which files to use with the -f flag. The order matters: Compose reads the files in the order they are provided, with later files overriding earlier ones.

# For development (if not using the auto-override)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up

# For production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Let's watch a practical, end-to-end example of this in action.

Docker + ReactJS tutorial: Development to Production workflow + multi-stage builds + docker compose

This video provides an excellent, detailed walkthrough of creating separate configurations for a React application, including multi-stage Dockerfiles and distinct Docker Compose files for development and production.

Watch from 1:11:50 to 1:23:01. The presenter refactors a single setup into a multi-file configuration. File Structure (1:11:50 - 1:16:57): Observe how they create a base docker-compose.yaml (though it ends up being mostly empty in this simple case, the principle stands), a docker-compose.dev.yaml for development specifics (like bind mounts), and a docker-compose.prod.yaml for production (using a different port and Dockerfile). Execution (1:16:57 - 1:23:01): Pay close attention to how the -f flag is used to combine the base file with the environment-specific one (docker compose -f ... -f ... up). Also, note the real-world problem he encounters and solves regarding build-time arguments (args) vs. runtime environment variables—a crucial distinction in production setups.

The key to remember about merging is that simple values (like an image name) are replaced, while lists (like ports or volumes) and dictionaries (like environment) are merged together.


3. A Cleaner Approach: The extends Keyword

While using multiple files with -f is very effective, it can sometimes lead to repetition if you have multiple services with similar base configurations. The extends keyword allows a service to inherit its configuration from another service, even one in a different file. This promotes the Don't Repeat Yourself (DRY) principle, a cornerstone of good engineering.

This approach is great for creating reusable service "templates."

How to Use Docker Compose Extends for Reusable ...

For a more structured and maintainable setup, the extends keyword is a powerful tool. The article 'How to Use Docker Compose Extends' provides a clear guide on this feature.

Please read the following sections: Basic Extends Usage: This introduces the syntax of extends, showing how to point to a file and service to inherit from. Environment-Based Configuration: This section provides a perfect example of our use case. It shows a base.yml with a generic app service, and then docker-compose.dev.yml and docker-compose.prod.yml files that each extend that base service to add environment-specific settings.

Using extends, your docker-compose.prod.yml might look like this, clearly stating its intent to inherit from a base and then apply production-specific overrides:

# docker-compose.prod.yml
services:
  app:
    extends:
      file: base.yml # The file containing the base service
      service: app   # The name of the service to extend
    image: myapp:${VERSION:-latest} # Override the image tag
    deploy:
      replicas: 3 # Add production-only deployment settings
    environment:
      - NODE_ENV=production
Test your understanding!

You have a docker-compose.yml file with a web service:

# docker-compose.yml
services:
  web:
    image: myapp:1.0
    ports:
      - "80:8000"
    environment:
      - LOG_LEVEL=INFO

You need to create a development override file (docker-compose.override.yml) that does the following:

  1. Changes the LOG_LEVEL to DEBUG.
  2. Adds a bind mount to sync your local ./src directory with /app/src in the container.
  3. Exposes port 5000 on the host to port 8000 in the container, in addition to the existing port mapping.

What would the contents of your docker-compose.override.yml be?

Show answer

The docker-compose.override.yml file would look like this:

# docker-compose.override.yml
services:
  web:
    ports:
      # This adds to the list from the base file, it does not replace it.
      - "5000:8000"
    volumes:
      # This adds a new volume mapping.
      - ./src:/app/src
    environment:
      # This dictionary is merged, and since LOG_LEVEL exists, its value is overridden.
      - LOG_LEVEL=DEBUG

When you run docker compose up, Compose will merge these files. The ports and volumes lists will be combined, and the LOG_LEVEL environment variable will be overridden. The final image will still be myapp:1.0 since it wasn't specified in the override file.


4. A Professional Project Structure

As a DevOps engineer, you'll often work on complex projects with many services. Combining include, extends, and multiple override files allows you to create a highly modular and maintainable structure.

How to Use Docker Compose Extends for Reusable ...

Let's see how these concepts come together in a professional, scalable project structure. The same article on extends shows an excellent template for this.

Review the sections Modular Project Structure, Complete Example, and Usage. You don't need to memorize every line, but focus on the organizational pattern: compose/: Contains a base.yml and environment-specific override files (development.yml, production.yml). services/: Defines individual services, which often extend from the base configurations. docker-compose.yml: The main file that uses include (another useful feature for modularity) to pull in all the service definitions. Usage: Note how the final commands combine the main file with the environment-specific override, just as we saw earlier.

This kind of structured approach is exactly what's needed for creating automated, reliable CI/CD pipelines that can build and deploy your application to any environment with a single command.


Conclusion

You have now learned the standard professional patterns for managing environment-specific configurations with Docker Compose. This is a powerful technique that separates concerns, reduces errors, and makes your application deployments far more robust and repeatable.

Key Takeaways:

  • Separate your base configuration from environment-specific overrides to maintain clarity and avoid errors.
  • Use a docker-compose.override.yml file for simple, automatic local development overrides.
  • Use the -f flag to explicitly combine Compose files for controlled environments like staging and production.
  • Leverage the extends keyword to create reusable service templates and keep your configurations DRY.
  • For complex applications, adopt a modular project structure that separates base configs, service definitions, and environment overrides.

In our next lesson, we'll take our production setup a step further. Now that we can define a clean production configuration, we'll learn how to scale services to run multiple container instances for high availability, a critical step in building resilient applications.