Skip to content

The compose.yaml File

Every Compose file has one required top-level key: services. Everything else — networks, volumes, configs — is optional.

services:
service-name:
image: some-image:tag
build: .
ports:
- "hostPort:containerPort"
environment:
- KEY=value
command: ["executable", "arg"]

Each key under services is a service name you choose. Compose starts one container per service by default.

services is a map. Each entry is a named service. The name becomes both the container identifier and the DNS hostname other services can use to reach it on the internal network.

services:
api:
image: node:22-alpine
db:
image: postgres:17-alpine

Here api and db are two services. Compose creates one container for each.

Use image to pull a pre-built image from a registry:

services:
web:
image: nginx:alpine

Use build to build an image from a local Dockerfile at build time:

services:
app:
build: .

You can combine them — build says where to find the Dockerfile, and image names the resulting image:

services:
app:
build: .
image: myapp:dev

ports maps a host port to a container port in "host:container" format. Always quote the mapping to avoid YAML interpreting the colon:

services:
web:
image: nginx:alpine
ports:
- "8080:80"
- "8443:443"

Pass environment variables into the container either as a list or a mapping:

# list form
environment:
- NODE_ENV=production
- PORT=3000
# mapping form
environment:
NODE_ENV: production
PORT: "3000"

Both forms are equivalent. The list form mirrors shell export syntax and is most common.

Override the default CMD from the image:

services:
worker:
image: myapp:latest
command: ["node", "worker.js"]

Pass a plain string for a shell-interpreted command:

services:
worker:
image: myapp:latest
command: node worker.js

Older tutorials show a version: "3.9" key at the top of the file. This is obsolete. The Compose Specification no longer requires or recommends the version key. Modern tooling ignores it. Omit it from all new files.

services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://db:5432/mydb
command: ["node", "src/server.js"]
db:
image: postgres:17-alpine
environment:
- POSTGRES_PASSWORD=secret

The snippet below writes a complete compose.yaml and starts it so you can inspect each field in action.

# Write a compose.yaml with two services
cat > compose.yaml <<'EOF'
services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"
    environment:
      - NGINX_HOST=localhost
    command: ["nginx", "-g", "daemon off;"]
  echo:
    image: alpine
    command: ["sh", "-c", "echo 'compose.yaml works!' && sleep 30"]
EOF

# Start both services
docker compose up -d

# Show running containers
docker compose ps

# Read echo output
docker compose logs echo

# Clean up
docker compose down
Which top-level key is required in every compose.yaml?
What is the correct way to map host port 9000 to container port 80 in compose.yaml?
Why should you omit the version: key from new compose.yaml files?
What does the build: . key do in a service definition?