Toolmingo
Guides11 min read

YAML Cheat Sheet: Syntax, Examples & Best Practices

Complete YAML cheat sheet — syntax, scalars, collections, multiline strings, anchors, aliases, and real-world examples for Docker Compose, Kubernetes, GitHub Actions, and Ansible.

YAML Cheat Sheet

YAML (YAML Ain't Markup Language) is a human-readable data serialization format used in Docker Compose, Kubernetes, GitHub Actions, Ansible, and CI/CD pipelines. This cheat sheet covers everything from basic syntax to production-grade patterns.

Quick Reference

Concept Syntax Example
Key-value key: value name: Alice
String (unquoted) key: text city: London
String (quoted) key: "text" msg: "Hello: World"
Integer key: 42 port: 8080
Float key: 3.14 version: 1.5
Boolean true / false enabled: true
Null null or ~ value: null
List (block) - item - nginx
List (inline) [a, b, c] [80, 443]
Map (inline) {k: v} {env: prod}
Comment # text # this is a comment
Anchor &name &defaults
Alias *name *defaults
Multiline literal | preserves newlines
Multiline folded > folds to single line
Document start --- separates documents
Document end ... optional terminator

Basic Syntax

Scalars

# Strings
name: Alice
greeting: "Hello, World!"    # quotes optional unless special chars
path: 'C:\Users\name'        # single quotes preserve backslashes
version: "1.0"               # quotes force string type (not number)

# Numbers
port: 8080
price: 9.99
negative: -42
scientific: 6.022e23

# Booleans
enabled: true
verbose: false
# Legacy YAML 1.1 also parsed: yes/no, on/off (avoid in modern YAML)

# Null
nothing: null
also_null: ~
missing:           # empty value = null

# Dates (ISO 8601)
created: 2026-07-15
timestamp: 2026-07-15T14:30:00Z

Strings That Need Quoting

# Quote when value contains YAML special characters
colon_val: "host:port"          # colon + space
hash_val: "value # not comment" # inline hash
bracket: "[not a list]"         # leading bracket
brace: "{not a map}"            # leading brace
bool_str: "true"                # force string, not boolean
null_str: "null"                # force string, not null
multi_word: "John Smith"        # optional but clear

# Use single quotes to disable escape sequences
regex: '^\d+\.\d+$'
windows: 'C:\Users\Alice\Desktop'

# Use double quotes for escape sequences
tab: "column1\tcolumn2"
newline_in_str: "line1\nline2"
unicode: "\u2603 snowman"

Collections

Sequences (Lists)

# Block style (recommended for readability)
fruits:
  - apple
  - banana
  - cherry

# Inline / flow style
fruits: [apple, banana, cherry]

# List of objects
servers:
  - name: web01
    ip: 10.0.0.1
    role: web
  - name: db01
    ip: 10.0.0.2
    role: database

# Nested lists
matrix:
  - [1, 2, 3]
  - [4, 5, 6]
  - [7, 8, 9]

# List as root element
- name: Alice
- name: Bob

Mappings (Dicts/Objects)

# Simple mapping
database:
  host: localhost
  port: 5432
  name: mydb
  ssl: true

# Inline / flow style
database: {host: localhost, port: 5432, name: mydb}

# Nested mappings
app:
  server:
    host: 0.0.0.0
    port: 8080
  database:
    host: db.example.com
    port: 5432
  cache:
    host: redis.example.com
    port: 6379

# Complex keys (rare)
? [complex, key]:
  value: something

Multiline Strings

Indicator Name Newlines Trailing newline
| Literal block preserved single
|- Literal strip preserved removed
|+ Literal keep preserved all kept
> Folded block → spaces single
>- Folded strip → spaces removed
>+ Folded keep → spaces all kept
# Literal block — newlines preserved
script: |
  #!/bin/bash
  echo "Hello"
  echo "World"
# Result: "#!/bin/bash\necho \"Hello\"\necho \"World\"\n"

# Folded block — newlines become spaces (good for long prose)
description: >
  This is a very long description
  that wraps across multiple lines
  but becomes a single paragraph.
# Result: "This is a very long description that wraps across multiple lines but becomes a single paragraph.\n"

# Strip trailing newline
message: |-
  No trailing
  newline here

# Indentation in literal blocks (extra indent relative to indicator)
sql: |
  SELECT *
  FROM users
  WHERE active = true
  ORDER BY name;

Anchors and Aliases

Anchors (&) define a reusable node; aliases (*) reference it.

# Define anchor
defaults: &defaults
  image: node:20
  working_dir: /app
  env:
    NODE_ENV: production

# Reference anchor (merges all keys)
web:
  <<: *defaults
  command: node server.js
  ports:
    - "3000:3000"

api:
  <<: *defaults
  command: node api.js
  ports:
    - "4000:4000"

# Override individual keys after merge
staging:
  <<: *defaults
  env:
    NODE_ENV: staging   # overrides the anchor's env.NODE_ENV

# Anchor a scalar
base_port: &port 8080
app_port: *port          # = 8080

# Anchor a list item
- &alice
  name: Alice
  role: admin
- name: Bob
  role: user
- *alice                 # exact copy of Alice

Multiple Documents

---
# Document 1
name: first
value: 1

---
# Document 2
name: second
value: 2

...
# Optional end-of-document marker

Tags and Explicit Types

# Force string type
zip_code: !!str 90210
number_string: !!str 42

# Force integer
count: !!int "007"

# Force float
ratio: !!float "1"   # = 1.0

# Binary data
logo: !!binary |
  R0lGODlhDAAMAIQAAP//9tX
  197atvNt9f5s+PqcFf4uBMJ...

# Python-specific (PyYAML)
set_example: !!python/object/apply:builtins.set
  - [1, 2, 3]

Real-World: Docker Compose

# docker-compose.yml
version: "3.9"

x-common-env: &common-env         # anchor for shared env
  TZ: UTC
  LOG_LEVEL: info

services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - static:/var/www/static
    depends_on:
      app:
        condition: service_healthy
    restart: unless-stopped
    environment:
      <<: *common-env

  app:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - NODE_ENV=production
    ports:
      - "3000:3000"
    environment:
      <<: *common-env
      DATABASE_URL: postgres://user:pass@db:5432/mydb
      REDIS_URL: redis://redis:6379
    depends_on:
      - db
      - redis
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb

  redis:
    image: redis:7-alpine
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru

volumes:
  pgdata:
  static:

networks:
  default:
    driver: bridge

Real-World: Kubernetes

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: production
  labels:
    app: my-app
    version: v2
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: my-app
        version: v2
    spec:
      containers:
        - name: app
          image: my-registry/my-app:v2.0.0
          ports:
            - containerPort: 3000
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
          env:
            - name: NODE_ENV
              value: production
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: password
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
      imagePullSecrets:
        - name: registry-secret
---
apiVersion: v1
kind: Service
metadata:
  name: my-app-svc
  namespace: production
spec:
  selector:
    app: my-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: ClusterIP

Real-World: GitHub Actions

# .github/workflows/ci.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  NODE_VERSION: "20"
  REGISTRY: ghcr.io

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: "npm"

      - run: npm ci
      - run: npm test
      - run: npm run lint

  build:
    name: Build & Push Docker Image
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Log in to registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ env.REGISTRY }}/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    name: Deploy to Production
    needs: build
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy
        run: |
          echo "Deploying to production..."
          # kubectl apply -f k8s/

Real-World: Ansible Playbook

# site.yml
---
- name: Configure web servers
  hosts: web
  become: true
  vars:
    app_port: 8080
    app_user: deploy
    log_level: info

  vars_files:
    - vars/secrets.yml    # ansible-vault encrypted

  pre_tasks:
    - name: Update apt cache
      apt:
        update_cache: true
        cache_valid_time: 3600
      when: ansible_os_family == "Debian"

  tasks:
    - name: Install packages
      package:
        name:
          - nginx
          - curl
          - git
        state: present

    - name: Create app user
      user:
        name: "{{ app_user }}"
        shell: /bin/bash
        create_home: true
        state: present

    - name: Deploy app config
      template:
        src: templates/app.conf.j2
        dest: /etc/app/config.conf
        owner: "{{ app_user }}"
        mode: "0640"
      notify: Restart app

    - name: Ensure service is running
      systemd:
        name: myapp
        state: started
        enabled: true

  handlers:
    - name: Restart app
      systemd:
        name: myapp
        state: restarted

YAML vs JSON vs TOML

Feature YAML JSON TOML
Human readable Excellent Good Good
Comments Yes (#) No Yes (#)
Multiline strings Yes (|, >) Escape \n Yes (""")
Anchors/aliases Yes No No
Trailing comma N/A No No
Types Rich Basic Rich
Indentation Significant No No
Multiple docs Yes (---) No No
Spec complexity High Low Medium
Common use cases DevOps configs APIs, data App configs
Parse complexity High Low Medium
File extension .yaml, .yml .json .toml

Common Mistakes

Mistake Wrong Correct
Tabs instead of spaces \tkey: val key: val (spaces only)
Inconsistent indentation Mixed 2/4 spaces Pick one, use consistently
Unquoted special chars msg: Hello: World msg: "Hello: World"
Truthy string parsed as bool enabled: yes enabled: "yes" (if string needed)
Quoted number as int port: "8080" port: 8080
Forgetting --- in K8s multi-doc Missing separator Add --- between resources
Merge key typo << : *anchor <<: *anchor
Wrong multiline for code script: > (folds lines) script: | (preserves newlines)

Validation & Tooling

# Validate YAML syntax
python3 -c "import yaml, sys; yaml.safe_load(sys.stdin)" < file.yaml

# yamllint (pip install yamllint)
yamllint file.yaml
yamllint -d relaxed file.yaml

# yq — jq for YAML (brew/snap/github.com/mikefarah/yq)
yq '.database.port' config.yaml          # read value
yq '.database.port = 5433' config.yaml   # update value
yq -o=json config.yaml                   # convert YAML → JSON
yq -P input.json                         # convert JSON → YAML (pretty)

# Kubernetes YAML validation
kubectl apply --dry-run=client -f deployment.yaml
kubectl apply --validate=true -f deployment.yaml

# Docker Compose validation
docker compose config    # validates and prints merged config

# GitHub Actions validation
# Install: npm i -g @action-validator/cli
action-validator .github/workflows/ci.yml

YAML in Python

import yaml

# Parse YAML string
data = yaml.safe_load("""
name: Alice
age: 30
roles:
  - admin
  - developer
""")
print(data['name'])  # Alice
print(data['roles'])  # ['admin', 'developer']

# Parse YAML file
with open('config.yaml') as f:
    config = yaml.safe_load(f)

# Dump Python object to YAML
config = {
    'database': {
        'host': 'localhost',
        'port': 5432,
    },
    'features': ['auth', 'api'],
}
yaml_str = yaml.dump(config, default_flow_style=False, sort_keys=False)
print(yaml_str)

# Write YAML file
with open('output.yaml', 'w') as f:
    yaml.dump(config, f, default_flow_style=False)

# Load multiple documents
with open('multi.yaml') as f:
    docs = list(yaml.safe_load_all(f))

# IMPORTANT: always use safe_load, never yaml.load() — prevents code execution

YAML in Node.js

// npm install js-yaml
const yaml = require('js-yaml');
const fs = require('fs');

// Parse YAML string
const data = yaml.load(`
name: Alice
port: 8080
tags:
  - web
  - api
`);
console.log(data.name); // Alice

// Parse YAML file
const config = yaml.load(fs.readFileSync('config.yaml', 'utf8'));

// Dump object to YAML
const obj = { server: { host: 'localhost', port: 3000 } };
const yamlStr = yaml.dump(obj, { indent: 2 });
fs.writeFileSync('output.yaml', yamlStr);

// Multiple documents
const docs = yaml.loadAll(fs.readFileSync('multi.yaml', 'utf8'));

YAML in Go

import "gopkg.in/yaml.v3"

type Config struct {
    Server struct {
        Host string `yaml:"host"`
        Port int    `yaml:"port"`
    } `yaml:"server"`
    Features []string `yaml:"features"`
}

// Unmarshal
data := `
server:
  host: localhost
  port: 8080
features:
  - auth
  - api
`
var cfg Config
if err := yaml.Unmarshal([]byte(data), &cfg); err != nil {
    log.Fatal(err)
}
fmt.Println(cfg.Server.Port) // 8080

// Marshal
out, _ := yaml.Marshal(cfg)
fmt.Println(string(out))

FAQ

Q: What's the difference between YAML 1.1 and YAML 1.2?

YAML 1.2 (2009) fixed the "Norway problem" and other quirks from 1.1. In YAML 1.1, NO was parsed as false, yes/no/on/off were booleans — breaking ISO country code NO (Norway). YAML 1.2 only recognizes true/false. Most tools (Go's yaml.v3, Python's PyYAML ≥6.0) now default to 1.2. Always use explicit true/false to be safe.

Q: When should I quote a string in YAML?

Quote when the value: (1) contains : (colon-space), #, {, }, [, ], ,, &, *, ?, |, >, !, %, @, `; (2) looks like a boolean (true, yes), null (null, ~), or number (42, 3.14); (3) starts or ends with whitespace; (4) is empty. Use single quotes '...' to disable escape sequences, double quotes "..." to enable them (\n, \t, \u2603).

Q: YAML indentation — tabs or spaces?

Spaces only. YAML explicitly forbids tabs for indentation. Any tab in indentation position is a parse error. Use 2 spaces (most common in K8s/Docker) or 4 spaces (common in Ansible). Pick one and be consistent within a file.

Q: How do anchors work with override (<<:)?

<<: is the YAML merge key. It merges all key-value pairs from the aliased mapping into the current mapping. Keys defined after <<: override merged keys. You can merge multiple anchors: <<: [*base, *extra]. Note: merge key is YAML 1.1 extension, not in 1.2 spec — but supported by virtually all tools.

Q: How do I represent multi-document YAML?

Separate documents with --- (three dashes). Optionally end each document with ... (three dots). Each document is independent — anchors don't span document boundaries. Kubernetes manifests commonly use multi-document YAML: kubectl apply -f manifests.yaml applies all documents in the file.

Q: Is YAML a superset of JSON?

YAML 1.2 is a strict superset of JSON — every valid JSON document is valid YAML. You can embed JSON objects and arrays directly in YAML files. The reverse is not true: YAML comments, anchors, multiline strings, and many types have no JSON equivalent. This is why yq -o=json works to convert YAML to JSON, but not all YAML can round-trip back.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools