Docker YAML: A Complete Guide to Writing Compose and Config Files
Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.
Every Docker-based deployment eventually comes down to a YAML file — a docker-compose.yaml, a docker-compose.yml, or a set of Compose overrides that define services, networks, and volumes. Understanding docker yaml syntax and structure is one of the most practical skills for anyone running containerized applications, whether on a laptop or a production VPS. This guide walks through the syntax rules, common patterns, and pitfalls of writing docker yaml files that actually work.
Docker itself doesn’t require YAML — the Docker Engine API and the docker run command work fine without it. YAML enters the picture through Docker Compose, which reads a docker yaml file to describe multi-container applications declaratively instead of as a sequence of shell commands. If you’ve ever pieced together a stack of docker run flags for a database, a backend, and a reverse proxy, you already understand the problem Compose — and its docker yaml format — solves.
Why Docker YAML Exists
Before Compose, running a multi-container application meant writing shell scripts full of docker run invocations, environment flags, and network commands. That approach doesn’t version well, doesn’t document intent clearly, and is error-prone when a teammate has to reproduce it. A docker yaml file solves this by describing the desired end state — which images to run, which ports to expose, which volumes to mount — in a single, readable, diffable text file.
YAML was chosen for this purpose (rather than JSON or a custom DSL) because it’s whitespace-sensitive, supports comments, and is relatively easy for humans to read and edit by hand. The tradeoff is that YAML’s indentation rules are strict and unforgiving — a single misplaced space can break an otherwise correct docker yaml file in ways that produce confusing error messages.
The Anatomy of a Basic Compose File
A minimal docker yaml file for Compose has three main top-level keys: services, networks, and volumes. Only services is required in practice, since Docker Compose will create sensible default networks and volumes if you don’t declare them explicitly.
services:
web:
image: nginx:1.27
ports:
- "8080:80"
depends_on:
- api
api:
build: ./api
environment:
- NODE_ENV=production
volumes:
- api-data:/app/data
volumes:
api-data:
Each service block maps to one container definition. The image key pulls a prebuilt image; build instead points Compose at a directory containing a Dockerfile. For a deeper comparison of when to use one versus the other, see this guide on Dockerfile vs Docker Compose.
Core Docker YAML Syntax Rules
Getting docker yaml indentation right is the single most common source of frustration for people new to Compose. YAML uses two-space indentation by convention (though any consistent number works), and — critically — it does not allow tabs. A file that mixes tabs and spaces will fail to parse, often with an error that doesn’t clearly point to the actual line at fault.
Some rules worth internalizing when writing docker yaml:
- item), and each item must be indented consistently under its parent key."3000:3000", which could otherwise be parsed as a number) should be quoted.yes, no, on, off) can be misinterpreted by some YAML parsers — quote them if you mean the literal string.# and are ignored by the parser, which makes them useful for documenting non-obvious configuration choices.Anchors and Aliases for Reducing Duplication
One underused feature of docker yaml is YAML’s native support for anchors (&) and aliases (*), which let you define a block once and reuse it elsewhere in the same file. This is particularly useful when several services share the same logging configuration, restart policy, or environment defaults.
x-common-env: &common-env
TZ: UTC
LOG_LEVEL: info
services:
worker:
image: myapp/worker:latest
environment:
<<: *common-env
QUEUE_NAME: default
scheduler:
image: myapp/scheduler:latest
environment:
<<: *common-env
QUEUE_NAME: scheduled
The x- prefix on x-common-env marks it as an extension field that Compose ignores when validating the schema, while still letting you reference it via the anchor. This pattern keeps a docker yaml file DRY without introducing an external templating tool.
Environment Variables Inside Docker YAML
Compose supports variable substitution directly in docker yaml files using ${VARIABLE} syntax, pulling values from the shell environment or a .env file placed next to the compose file. This is the standard way to keep secrets and environment-specific values out of the committed docker yaml itself.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
ports:
- "${DB_PORT:-5432}:5432"
The ${DB_PORT:-5432} syntax provides a default value if the variable isn’t set, which is a good practice for making a docker yaml file portable across environments. For a full treatment of this topic, this guide on managing Docker Compose environment variables covers precedence rules and common gotchas in more depth, and there’s a companion piece specifically on Docker Compose environment variable behavior worth reading alongside it.
Structuring Multi-Service Docker YAML Files
As an application grows past two or three containers, a single flat docker yaml file starts to get unwieldy. Compose supports splitting configuration across multiple files using the -f flag or a include directive, letting you keep a base file plus environment-specific overrides.
docker compose -f docker-compose.yaml -f docker-compose.prod.yaml up -d
This pattern is common for teams that need slightly different settings between local development and production — for example, mounting source code as a bind volume locally but not in production, or exposing debug ports only in a dev override file.
Networks in Docker YAML
By default, Compose creates a single bridge network for all services defined in a docker yaml file, letting them reach each other by service name as a DNS hostname. You can also define custom networks explicitly, which is useful for isolating a database tier from a public-facing web tier.
services:
web:
image: myapp/web:latest
networks:
- frontend
db:
image: postgres:16
networks:
- backend
networks:
frontend:
backend:
With this docker yaml layout, web and db cannot reach each other directly unless a third service bridges both networks — a deliberate way to reduce the blast radius of a compromised container.
Volumes and Persistent Data
Named volumes declared in a docker yaml file persist data outside the container’s writable layer, which matters for anything you don’t want wiped out when a container is recreated. If you’re setting up a database like Postgres or Redis, getting the volume definition right in your docker yaml is essential — see this dedicated Postgres Docker Compose setup guide or the Redis Docker Compose guide for concrete, tested examples.
services:
db:
image: postgres:16
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
driver: local
Validating and Debugging a Docker YAML File
Because YAML errors can be cryptic, it’s worth validating a docker yaml file before deploying it. Compose ships a built-in validator that resolves variable substitution and anchors, then prints the final, normalized configuration.
docker compose config
Running this command against your docker yaml file surfaces indentation errors, undefined variables, and invalid keys before you ever try to start containers. It’s a good habit to run docker compose config as a pre-deploy check in CI, catching a broken docker yaml file before it reaches a server.
Common Docker YAML Mistakes
A few mistakes recur often enough to call out specifically:
80:80 to be parsed unexpectedly.version key from older Compose file formats — newer versions of Compose largely ignore it, and the Docker Compose file reference no longer requires it.depends_on controls start order, not readiness — a database container can report “running” before it’s actually accepting connections.Rebuilding After a Docker YAML Change
Editing a docker yaml file doesn’t automatically rebuild images that reference local Dockerfiles — you generally need to force a rebuild after changing build context or Dockerfile instructions. The Docker Compose rebuild guide and the Docker Compose build guide both cover the exact commands and flags for this, including when --no-cache is actually necessary versus when it just wastes build time.
Docker YAML in Production Environments
Running a docker yaml-defined stack in production introduces a few additional considerations beyond local development. Restart policies (restart: unless-stopped or restart: always) keep services running across host reboots. Resource limits (deploy.resources.limits in Swarm mode, or mem_limit/cpus in plain Compose) prevent a single misbehaving container from starving the rest of the host.
services:
api:
image: myapp/api:latest
restart: unless-stopped
mem_limit: 512m
cpus: 0.5
If you’re deploying this kind of stack on a self-managed server, the underlying VPS still needs proper resource headroom and a sane security baseline — an unmanaged VPS hosting guide is a useful starting point for that side of the setup. For providers, DigitalOcean and Hetzner are both commonly used for small-to-mid-sized Compose deployments, largely because their pricing scales reasonably with the modest resource footprints most docker yaml stacks need.
Secrets Management in Docker YAML
Plaintext passwords in a committed docker yaml file are a recurring security problem. Compose supports a secrets top-level key that can reference external files or Docker Swarm secrets instead of inlining sensitive values directly. The Docker Compose secrets guide walks through this mechanism in detail, including how it differs from just using an .env file.
services:
db:
image: postgres:16
secrets:
- db_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Logging Configuration
A docker yaml file can also control how container logs are collected and rotated, which matters once a stack has been running long enough to fill a disk with unrotated log files. If you’re debugging a stack, the Docker Compose logs guide and the related Docker Compose logging setup guide cover both the docker compose logs command and the logging key you can add to any service block in your docker yaml.
services:
api:
image: myapp/api:latest
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Docker YAML Beyond Compose
It’s worth noting that “docker yaml” isn’t limited to Compose files. Kubernetes manifests are also YAML, and teams that outgrow a single-host Compose deployment often migrate toward Kubernetes for orchestration across multiple machines. The two formats look superficially similar but serve different purposes — Compose describes a fixed set of containers on one host, while Kubernetes YAML describes desired state across a cluster with its own scheduler. If you’re weighing that decision, this Kubernetes vs Docker Compose comparison lays out the tradeoffs in more detail. The official Kubernetes documentation is the authoritative reference if you do move in that direction.
Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.
FAQ
What’s the difference between docker-compose.yml and docker-compose.yaml?
Both extensions are valid and functionally identical — Compose accepts either .yml or .yaml. The choice is purely stylistic; picking one and staying consistent across a project matters more than which one you choose.
Do I need to specify a version key in a docker yaml file?
Modern versions of Docker Compose (the docker compose CLI plugin, not the older standalone docker-compose binary) largely ignore the version key and infer the schema from the file’s structure. Omitting it is generally fine with current Compose releases, though older tooling may still expect it.
Why does my docker yaml file fail to parse with no clear error?
This is almost always an indentation issue — either a stray tab character or inconsistent spacing between sibling keys. Running docker compose config will usually surface the specific line, and most text editors can be configured to visibly highlight tabs versus spaces to catch this before it happens.
Can I use one docker yaml file for both development and production?
You can, but it’s usually cleaner to use a base file plus an override file passed via -f, so environment-specific settings (bind mounts, debug ports, resource limits) don’t have to be commented in and out manually.
Conclusion
A docker yaml file is ultimately just a structured, readable description of the containers, networks, and volumes an application needs — but small syntax mistakes can cause outsized confusion given how strict YAML indentation rules are. Sticking to consistent spacing, validating with docker compose config before deploying, and splitting configuration across base and override files as a project grows will keep a docker yaml setup maintainable well past the point where a single flat file would have become unmanageable. The official Docker Compose specification remains the definitive reference for every key discussed here, and is worth bookmarking for whenever a less common option comes up.
Leave a Reply