Back to BlogDocker compose · Odoo · docker

Understanding docker-compose.yml

2025-12-22

The docker-compose.yml file is a YAML configuration file that defines how your multi-container applications should be structured and run. This file serves as the blueprint for your application's architecture, specifying everything from container configurations to resource constraints. In this article, we'll explore the anatomy of this file and understand how to leverage its capabilities effectively.

The Anatomy of docker-compose.yml

At its core, a docker-compose.yml file is organized into several top-level keys that define different aspects of your application. The file follows YAML syntax rules, where indentation matters and defines the hierarchy of configuration options.

Version Specification

While the version key was commonly used in older Compose files, modern Docker Compose (v2 and above) has made this optional. The version determined which features were available, but current implementations automatically detect and support the latest specification format.

version: '3.8'  # Optional in modern Compose

Top-Level Keys

The main structure of a docker-compose.yml file includes several top-level configuration sections:

  • services: Defines the containers that make up your application
  • volumes: Declares named volumes for persistent data storage
  • networks: Specifies custom network configurations
  • configs: Manages configuration files (primarily for Swarm mode)
  • secrets: Handles sensitive data securely

Service Definitions

The services section is where you define each container in your application. Each service represents a containerized component of your system.

Basic Service Structure

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"

This minimal configuration defines a service named "web" that uses the nginx image and maps port 8080 on your host to port 80 in the container.

Build Context

Instead of using a pre-built image, you can specify a build context to create an image from source:

services:
  app:
    build:
      context: ./app
      dockerfile: Dockerfile.prod
      args:
        BUILD_VERSION: "1.0"

The context specifies the directory containing your build files, dockerfile points to a specific Dockerfile if it's not named "Dockerfile", and args passes build-time variables.

Container Naming

You can explicitly name your containers for easier identification:

services:
  database:
    image: postgres:14
    container_name: my_postgres_db

Without container_name, Compose generates names using the pattern: projectname_servicename_number.

Port Mapping and Exposure

Port configuration controls how your containers communicate with the host system and each other.

Publishing Ports

The ports key publishes container ports to the host:

services:
  api:
    image: myapi:latest
    ports:
      - "3000:3000"        # host:container
      - "127.0.0.1:3001:3001"  # bind to localhost only
      - "4000-4005:4000-4005"  # port range

Exposing Ports

The expose key makes ports accessible to linked services without publishing them to the host:

services:
  backend:
    image: mybackend:latest
    expose:
      - "8000"

Dependency Management

The depends_on key controls the startup order of services:

services:
  web:
    image: webapp:latest
    depends_on:
      - database
      - cache
  
  database:
    image: postgres:14
  
  cache:
    image: redis:alpine

This ensures that database and cache start before web. However, depends_on only waits for containers to start, not for them to be ready to accept connections.

Conditional Dependencies

For more control over startup conditions:

services:
  app:
    image: myapp:latest
    depends_on:
      db:
        condition: service_healthy
  
  db:
    image: postgres:14
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

Resource Constraints

You can limit the resources available to each container to prevent any single service from consuming excessive system resources.

Memory Limits

services:
  processor:
    image: dataprocessor:latest
    mem_limit: 512m
    mem_reservation: 256m

The mem_limit sets a hard limit, while mem_reservation sets a soft limit that the container should stay within under normal conditions.

CPU Constraints

services:
  compute:
    image: compute-intensive:latest
    cpus: '0.5'
    cpu_shares: 512

The cpus value limits the container to 50% of one CPU core, while cpu_shares sets the relative CPU priority compared to other containers.

Restart Policies

Restart policies determine how containers behave when they exit:

services:
  api:
    image: api:latest
    restart: unless-stopped

Available restart policies:

  • no: Never restart (default)
  • always: Always restart regardless of exit status
  • on-failure: Restart only on failure (non-zero exit code)
  • unless-stopped: Always restart unless explicitly stopped

You can also specify a maximum retry count:

services:
  worker:
    image: worker:latest
    restart: on-failure:3

Command and Entrypoint Overrides

You can override the default command or entrypoint defined in the image:

services:
  app:
    image: myapp:latest
    command: python manage.py runserver 0.0.0.0:8000
    entrypoint: /custom-entrypoint.sh

Commands can also be specified as arrays:

services:
  processor:
    image: processor:latest
    command: ["python", "processor.py", "--mode", "production"]

Working Directory and User

Specify the working directory and user context for the container:

services:
  app:
    image: nodeapp:latest
    working_dir: /usr/src/app
    user: "1000:1000"

This runs the container's process from the specified directory and as a specific user (by UID:GID).

Labels and Metadata

Labels add metadata to containers for organization and automation:

services:
  web:
    image: nginx:latest
    labels:
      com.example.description: "Frontend web server"
      com.example.department: "engineering"
      com.example.version: "1.0"

Labels are key-value pairs that can be queried and filtered by various tools and scripts.

Advanced Service Configurations

Privileged Mode

Some containers require elevated privileges:

services:
  system_monitor:
    image: monitor:latest
    privileged: true

Use this cautiously as it grants the container nearly all capabilities of the host machine.

Device Mapping

Map host devices into containers:

services:
  hardware_access:
    image: hwcontrol:latest
    devices:
      - "/dev/video0:/dev/video0"
      - "/dev/sda:/dev/xvda:rwm"

PID and Network Modes

Share process or network namespaces:

services:
  debugger:
    image: debug:latest
    pid: "host"  # Share host's PID namespace
  
  nettools:
    image: tools:latest
    network_mode: "host"  # Use host networking

Extension Fields and YAML Anchors

Reduce repetition using YAML anchors and extension fields:

x-common-variables: &common-env
  LOG_LEVEL: info
  APP_ENV: production

x-common-config: &common-config
  restart: unless-stopped
  mem_limit: 512m

services:
  api:
    <<: *common-config
    image: api:latest
    environment:
      <<: *common-env
      SERVICE_NAME: api
  
  worker:
    <<: *common-config
    image: worker:latest
    environment:
      <<: *common-env
      SERVICE_NAME: worker

Extension fields (starting with x-) are ignored by Compose but can be referenced using anchors (&) and merged using aliases (* and <<).

Profiles for Conditional Service Activation

Profiles allow you to selectively start services:

services:
  web:
    image: webapp:latest
  
  debug_tools:
    image: debug:latest
    profiles:
      - debug
  
  testing_db:
    image: postgres:latest
    profiles:
      - testing

Services with profiles only start when the profile is explicitly activated through the command line or environment variable.

Multiple Compose Files

You can split your configuration across multiple files for better organization:

# docker-compose.yml (base)
services:
  app:
    image: myapp:latest
    ports:
      - "8000:8000"

# docker-compose.override.yml (automatically merged)
services:
  app:
    environment:
      DEBUG: "true"

Compose automatically merges docker-compose.override.yml with docker-compose.yml. You can also specify additional files explicitly.

Configuration Validation

The docker-compose.yml file structure must follow specific rules. Indentation must be consistent (typically 2 spaces), and keys must be properly nested. Invalid YAML syntax or incorrect key names will cause Compose to reject the file with an error message indicating the problem location.

Best Practices for Organizing Your Compose File

Structure your docker-compose.yml file logically by grouping related services together. Place frequently modified configurations like port mappings and volume definitions near the top for easy access. Use comments to document non-obvious configurations:

services:
  app:
    image: myapp:latest
    # Binding to localhost only for security
    ports:
      - "127.0.0.1:8000:8000"
    # Increased memory for data processing tasks
    mem_limit: 1g

Variable Interpolation

Reference shell environment variables or variables from external files:

services:
  web:
    image: nginx:${NGINX_VERSION:-latest}
    ports:
      - "${EXTERNAL_PORT:-8080}:80"

The syntax ${VARIABLE:-default} provides a fallback value if the variable isn't set.

Understanding the File's Role in Your Workflow

The docker-compose.yml file acts as infrastructure-as-code for your application. It documents your application's architecture, makes your setup reproducible across different environments, and provides a single source of truth for your application's configuration. By mastering this file's structure and capabilities, you gain precise control over how your containerized applications are configured and deployed.

We use cookies to improve your experience and analyse site traffic. See our Privacy Policy.