Table of Contents
Understanding the `docker-compose.yml` File
The docker-compose.yml file is the central definition of your multi container application. It describes which containers exist, how they relate to each other, how they connect to networks and volumes, and which configuration each one receives. In this chapter you focus on the structure of this file, how it is organized, and how Docker Compose interprets it.
YAML Basics in the Context of Docker Compose
The docker-compose.yml file uses YAML syntax. YAML is a structured, indentation based format that represents maps (key value pairs), lists, and simple values such as strings and numbers. In Docker Compose, indentation controls nesting. A key followed by a colon starts a new mapping. Values under that key are indented one level deeper. Lists are written with a hyphen, also indented under a key.
Always keep indentation consistent in docker-compose.yml. Wrong indentation changes the structure and can cause confusing errors or unexpected behavior.
Docker Compose files typically live at the root of a project, named docker-compose.yml. A secondary file name like docker-compose.override.yml can be used for overrides, but the main structural definition always comes from a compose file using YAML.
Top Level Keys and File Layout
At the top level, a modern compose file usually contains a small set of keys. The most important one is services, which defines your containers. Other common top level keys are volumes, networks, and sometimes configs or secrets in more advanced use cases.
A minimal layout looks conceptually like this, focusing only on structure:
version: "3.9"
services:
service_name_1:
# service configuration
service_name_2:
# service configuration
volumes:
volume_name_1:
# volume definition
networks:
network_name_1:
# network definition
The version field describes the compose file format version. Newer Docker Compose implementations often accept files without an explicit version, but you will still see it used in many examples for clarity.
The services section always appears as a mapping of service names to their configurations. The volumes and networks sections are mappings of named resources to their definitions. These top level blocks organize all other details in a predictable hierarchy.
The `services` Section
The heart of docker-compose.yml is the services key. Each entry under services defines one containerized component of your application. The name you choose for each service acts as an identifier. This name is used in other sections, such as network configuration or when one service depends on another.
Each service definition contains keys that describe how that container should be created. These keys include the container image, the command to run, ports, environment configuration, volume mounts, and which networks the container joins. While this chapter does not describe the behavior of each individual option in depth, it is important to understand where they live in the structure.
Conceptually, a service entry looks like this:
services:
app:
image: some-image:tag
ports:
- "8080:80"
environment:
- EXAMPLE=value
volumes:
- app-data:/data
networks:
- frontend
db:
image: some-db-image:tag
volumes:
- db-data:/var/lib/db
networks:
- backend
Here app and db are service names. Under each one, keys such as image, ports, environment, volumes, and networks express how that container is configured. This nested structure is the core pattern that you repeat for every service you add.
The `volumes` Section
The volumes section at the top level defines named volumes that services can use. Instead of describing behavior, this section describes resources that services refer to later. A volume can be defined with no additional keys for default behavior, or it can include driver and configuration details.
In structural terms, it looks like this:
volumes:
app-data:
# optional volume settings
db-data:
# optional volume settings
Later in the file, services reference these names under their own volumes key. This separation between defining volumes at the top level and referencing them inside services is part of the docker-compose.yml structure. It helps keep resource definitions in a single place and makes reuse easier.
The `networks` Section
Similarly, the networks section defines named networks your services can join. At the top level you declare networks and optionally specify their drivers or other settings. This is the structural place where all custom network definitions live.
A structural representation is:
networks:
frontend:
# optional network settings
backend:
# optional network settings
Services refer to these network names under their networks key. This two level pattern, define at the top and reference inside services, is one of the key structural ideas in Docker Compose files.
Common Structural Patterns Inside a Service
Inside each service, Docker Compose supports many configuration keys. Even though each key has its own meaning, they are all arranged as nested mappings or lists directly under the service name. Some of the most typical structural patterns are:
A simple scalar value written directly under the service, such as image:, build:, or restart:.
A list under a key, for example ports:, volumes:, or depends_on:. Each item in the list is written with a hyphen.
A mapping under a key, for example environment: when you use key value pairs instead of list form, or deploy: in more advanced configurations.
An example that illustrates all these forms:
services:
web:
image: my-web-image:latest # scalar
ports: # list
- "80:80"
- "443:443"
environment: # mapping
APP_ENV: production
APP_DEBUG: "false"
depends_on: # list
- db
- cacheThe structure is always: service name, followed by a block of keys, each indented, with their values defined as either scalars, lists, or mappings.
Within a service, every configuration key must be indented exactly one level under the service name. Do not align service keys with the services key itself. Misaligned keys will be treated as new top level sections or cause parsing errors.
String Forms and Short Syntax
Many service keys provide both long and short forms. This affects structure, not just style. For example, environment can be a list of single strings in the form KEY=VALUE or a mapping where keys and values are separated by a colon. Both influence how you indent and write the YAML.
As list syntax:
environment:
- APP_ENV=production
- APP_DEBUG=falseAs mapping syntax:
environment:
APP_ENV: production
APP_DEBUG: "false"
Compose parses both correctly. The choice affects readability and consistency, but not the overall tree. The same idea appears in other keys for example volumes and ports often use a short string format to express multiple values in a single line, even though you still write them as list items under their key.
Service Dependencies and References
The structure of docker-compose.yml allows services to reference each other by name. Keys such as depends_on accept service names defined elsewhere in the services block. This means that the file is a connected graph of references, but it still maintains a hierarchical shape.
A simple example:
services:
api:
image: my-api:latest
depends_on:
- db
db:
image: my-db:latest
Here api and db are siblings under services. The depends_on key of api contains a list item db. This relationship is entirely expressed by structure and naming inside the same docker-compose.yml file.
Combining Top Level Resources with Services
The full structural picture of a typical compose file brings together services, volumes, and networks. Services define how containers run, while volumes and networks provide named infrastructure. Services reference these names, which ties the sections together.
A compact structural example:
version: "3.9"
services:
app:
image: my-app:latest
volumes:
- app-data:/app/data
networks:
- app-net
db:
image: my-db:latest
volumes:
- db-data:/var/lib/postgresql/data
networks:
- app-net
volumes:
app-data: {}
db-data: {}
networks:
app-net: {}
Even without discussing the detailed behavior of each option, you can see the pattern. All services are grouped under services. All named volumes are grouped under volumes. All named networks are grouped under networks. Indentation expresses parent child relationships throughout. Empty mappings such as {} indicate default configuration but keep the structure explicit.
File Variants and Extending Structure
While you mostly work with a single docker-compose.yml, the structure is designed to be extended by combining multiple files. Overrides and environment specific configurations often live in separate compose files that Docker Compose merges at runtime. Each file still follows the same structural rules, with services, volumes, and networks as top level keys.
The merge process respects these structural units. Services with the same name across files are combined. Volumes and networks are merged by name. This means that a consistent and clear structure in each file is essential when you start layering configurations for different environments.
Structural Consistency and Readability
Beyond correct YAML syntax, good structure in docker-compose.yml also means organizing related settings in a clear and predictable way. Service names should reflect their roles. Indentation and ordering should be consistent across all services. Top level sections should be grouped so that someone reading the file can quickly see what services exist and which shared resources they use.
A clear, consistent structure in docker-compose.yml is not only about avoiding syntax errors. It is also a critical part of making multi container setups understandable and maintainable for you and your team.
By focusing on the shape of the file the hierarchy of keys, the use of indentation, and the separation between services, volumes, and networks you create a stable foundation that makes later configuration changes much easier to reason about.