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.