How to Include One YAML File Inside Another (In Docker Compose, K8s, CI/CD)

YAML has no native file inclusion mechanism.

There is no !include, no import, no require. The YAML specification deliberately avoids defining how files reference each other — that responsibility belongs to the tools consuming the YAML, not the format itself.

For developers maintaining large infrastructure repositories, this creates a real problem. A monolithic Kubernetes manifest for a production service can easily exceed 500 lines. A Docker Compose file with five microservices plus databases plus caches is unmanageable without splitting it.

Every platform that uses YAML eventually builds its own file inclusion mechanism. Some are elegant. Some are hacked together. None are standardized.

This guide covers how every major YAML-consuming tool handles file includes, and how to compose YAML files when your tool does not support it natively.


Why YAML Has No Native Include

The YAML specification is a data serialization language, not a build system. File inclusion is a content management problem, not a serialization problem.

The spec's philosophy: if a tool needs to compose YAML from multiple files, that tool should define its own mechanism rather than adding complexity to the core language.

This sounds reasonable until you are maintaining a 2000-line GitHub Actions workflow and desperately wishing for a simple import: ./shared-steps.yml.


Docker Compose: extends and Multiple Compose Files

Docker Compose offers two inclusion mechanisms.

extends (Legacy but Still Used)

The extends field references a service defined in another file:

# docker-compose.common.yml
services:
  app-base:
    image: node:20-alpine
    restart: unless-stopped
    networks:
      - app-network
# docker-compose.services.yml
services:
  api:
    extends:
      file: docker-compose.common.yml
      service: app-base
    environment:
      - PORT=3000

  worker:
    extends:
      file: docker-compose.common.yml
      service: app-base
    command: npm run worker

Docker Compose resolves extends at runtime, pulling the base service's configuration and merging it with the local definition. Local keys override inherited ones.

Caveat: extends is deprecated in newer Docker Compose versions but still widely documented and used. The recommended replacement is multiple compose files.

Multiple Compose Files

docker compose -f docker-compose.base.yml -f docker-compose.prod.yml up

The second file overlays onto the first. Keys in the second file override the first:

# docker-compose.base.yml
services:
  api:
    image: api:latest
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
# docker-compose.prod.yml
services:
  api:
    environment:
      - NODE_ENV=production
      - DB_HOST=prod-db.internal

The result is a combined configuration with NODE_ENV=production overriding the base value, and DB_HOST added.

This is the most practical inclusion mechanism for Docker Compose because it requires no special syntax — just shell-level file concatenation.


Kubernetes: Kustomize and Helm

Kubernetes does not support "including" one YAML file inside another. Instead, the ecosystem provides two powerful composition tools.

Kustomize

Kustomize is Kubernetes-native configuration customization. It does not use templates — it patches and overlays base YAML files.

# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - deployment.yaml
  - service.yaml
  - configmap.yaml
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../base

patches:
  - path: replica-patch.yaml

Kustomize composes multiple YAML files into a single output by layering patches onto base resources. This is effectively file inclusion with override capabilities.

Helm

Helm uses Go templates with include and template directives:

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: {{ .Values.appName }}
          env:
            {{- include "app.env" . | indent 10 }}

# templates/_helpers.tpl
{{- define "app.env" }}
- name: DB_HOST
  value: {{ .Values.db.host }}
- name: DB_PORT
  value: {{ .Values.db.port | quote }}
{{- end }}

Helm's include is a template directive, not a YAML feature, but it achieves the same goal — reusable YAML blocks composed from separate definitions.


Ansible: include_tasks and import_playbook

Ansible's YAML-based playbooks have the richest include system in the YAML ecosystem.

# main.yml
- name: Configure web server
  hosts: webservers
  tasks:
    - include_tasks: setup.yml
    - include_tasks: deploy.yml
    - include_tasks: healthcheck.yml
# setup.yml
- name: Install packages
  apt:
    name: nginx
    state: present

- name: Create config directories
  file:
    path: /etc/nginx/sites-enabled
    state: directory

Ansible also supports dynamic includes with include_vars:

- name: Load environment-specific variables
  include_vars: "{{ env }}.yml"

This loads production.yml, staging.yml, or development.yml based on the env variable — true parameterized file inclusion.


GitHub Actions: Reusable Workflows and Composite Actions

GitHub Actions solves file composition through two mechanisms.

Reusable Workflows

# .github/workflows/deploy.yml
jobs:
  deploy:
    uses: ./.github/workflows/shared-deploy.yml
    with:
      environment: production

The uses keyword calls another workflow file as a job. The caller passes inputs that the reusable workflow receives as parameters.

Composite Actions

# .github/actions/setup/action.yml
name: "Setup"
description: "Setup Node and install dependencies"
inputs:
  node-version:
    description: "Node.js version"
    required: true

runs:
  using: "composite"
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
    - run: npm ci
      shell: bash

Workflows include composite actions as steps:

jobs:
  build:
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup
        with:
          node-version: 20
      - run: npm run build

CI/CD Pipeline Inclusion Patterns

GitLab CI: include

GitLab CI has one of the cleanest inclusion systems:

# .gitlab-ci.yml
include:
  - local: 'templates/common.yml'
  - project: 'myteam/shared-ci'
    file: 'pipelines/deploy.yml'
  - remote: 'https://example.com/templates/security.yml'

Includes are resolved before the pipeline runs, merging the included files into the final configuration.

CircleCI: orbs and executors

version: 2.1

orbs:
  node: circleci/node@5.0

executors:
  default:
    docker:
      - image: cimg/node:20.0

jobs:
  build:
    executor: default
    steps:
      - node/install

CircleCI's orb system is essentially a distributed include mechanism with versioning and namespace isolation.


Preprocessor-Based Includes

When your tool does not support includes natively, use a preprocessor.

yq-based include

yq is a YAML processor that can merge files:

yq eval-all '. as $item ireduce ({}; . *+ $item)' base.yml override.yml

This deep-merges two YAML files, with the second overriding the first.

Custom preprocessor with anchors

Define an include convention using anchors:

# config.yml
defaults: &defaults
  app_name: myapp
  port: 8080

# Include via preprocessor
#include: partials/database.yml

services:
  web:
    <<: *defaults

Then run a script that replaces #include: path with the file content before parsing:

python preprocess.py config.yml | yamllint -

M4 macro preprocessor

For extreme cases, some teams use m4 — the Unix macro processor — to include files before YAML parsing:

m4 -I ./partials config.yml.m4 > config.yml

This is overkill for most projects but works when you need file inclusion across many YAML files in a monorepo.


When You Should NOT Include Files

File inclusion is not always the right answer.

Too many small files. Splitting a 50-line config into 10 files of 5 lines each makes configuration harder to understand, not easier.

Cross-tool incompatibility. If you use a Docker Compose extends pattern that only works in Compose, you cannot reuse those files in Kubernetes or GitHub Actions.

Versioning complexity. Reusable YAML files that are included by multiple projects create versioning and dependency management challenges that outweigh the benefits of deduplication.

Debugging difficulty. Included files make it harder to trace configuration issues because the final resolved YAML is not visible in any single file.

The rule of thumb: if your base YAML file exceeds 300 lines and contains clearly separable concerns (databases vs application vs monitoring), splitting with includes is worth it. If your file is under 100 lines, keep it monolithic.


FAQ

Can you include one YAML file in another?

The YAML specification does not define any include or import directive. However, most platforms that consume YAML files provide their own inclusion mechanisms. Docker Compose supports extends and multi-file composition with docker compose -f. Kubernetes uses Kustomize for resource composition or Helm templates for Go-template includes. Ansible has include_tasks and import_playbook. GitLab CI has an include keyword. GitHub Actions supports reusable workflows and composite actions. If your platform lacks native includes, you can use a preprocessing tool like yq to merge YAML files before parsing.

How do I split Docker Compose YAML into multiple files?

Docker Compose supports multiple compose files natively. Define common configuration in a docker-compose.base.yml file, then create environment-specific files that override only the values that differ. Run with docker compose -f docker-compose.base.yml -f docker-compose.prod.yml up. The second file overlays onto the first — service definitions, environment variables, and other settings in the second file merge with or override values from the first. This is the recommended approach over the legacy extends directive.

How do I import a YAML file in Kubernetes?

Kubernetes does not support YAML file import natively. The standard approach is Kustomize: define base resources in one directory and create overlays that reference and patch those bases. Kustomize is built into kubectl (kubectl kustomize) and handles composition, merging, and patching of Kubernetes YAML files. For template-based composition, Helm uses Go template include and template directives to compose reusable YAML fragments from _helpers.tpl files.

Does GitHub Actions support YAML file includes?

Yes, through two mechanisms. Reusable workflows let you reference another .github/workflows/*.yml file as a job using the uses keyword, passing inputs and receiving outputs. Composite actions let you define reusable step sequences in .github/actions/*/action.yml files that workflows reference as single steps. Both mechanisms enable modular workflow composition without duplicating configuration across multiple workflow files.

What is the best way to share YAML configuration across projects?

The best approach depends on your tooling. For Docker Compose, use multi-file composition. For Kubernetes, use Kustomize overlays or Helm charts published to a registry. For CI/CD pipelines, use reusable workflows or includes (GitHub Actions reusable workflows, GitLab CI include, CircleCI orbs). For general-purpose YAML across unrelated tools, consider a preprocessing step using yq or a custom script that concatenates or merges files before the YAML parser reads them. Avoid inventing custom include syntax — use the include mechanism your platform provides, or none at all.


Final Thoughts

YAML's lack of native file inclusion is frustrating, but every major platform has built reasonable solutions on top of it.

The key is to use the inclusion mechanism your platform provides, not to fight against it. Docker Compose multi-file composition, Kubernetes Kustomize, and GitHub Actions reusable workflows all work well within their ecosystems.

If you are stuck with a tool that does not support includes and your YAML files are growing unmanageable, a simple preprocessing step with yq or a shell script is better than living with a 2000-line configuration file.

And when you are debugging an included YAML file that is not resolving as expected, paste the final composed output into a YAML formatter — seeing the resolved configuration makes it immediately clear which file's values took precedence.

For more on indentation issues that commonly arise when composing YAML files, see How to Fix YAML Indentation Errors and Why Your YAML Is Invalid. If you are looking for a quick way to validate composed configs, Best YAML Formatter Tools covers the most reliable validators.