Docker Compose Is Documentation That Actually Runs

Source: Dev.to | Jan 28, 2026 (UTC)

Two projects. Both have Dockerfiles. One takes me 20 minutes to figure out how to run. The other – 30 seconds.

The difference? The second one has a docker-compose.yml.

That file told me everything: what ports to expose, what services it depends on, what environment variables it needs, what volumes to mount. The Dockerfile only told me how to build.

Compose isn’t just a convenience tool. It’s executable documentation.

The “I’ll Just Remember It” Trap

A year ago I was debugging a service that hadn’t been touched in months. The Dockerfile was there. Clean, multi-stage, optimized. Professional.

No Compose file. Just a Dockerfile and a README that said “see Dockerfile.”

How do I run it?

docker build -t myservice . docker run myservice

Enter fullscreen mode

Exit fullscreen mode

Container crashes. Missing environment variables. Okay, which ones?

I open config/config.go. There’s a struct with 15 fields loaded via envconfig. Three have defaults. The rest? Required, apparently. The README mentions DATABASE_URL and that’s it – the rest are “obvious.”

I check git history. Find a Slack message from six months ago:

“run it with -e DATABASE_URL=... -e REDIS_URL=... -e SECRET_KEY=... -v $(pwd)/config:/app/config -p 8080:8080 --network=backend“

This is not engineering. This is archeology.

Compose as the Single Source of Truth

Now compare:

services: myservice: build: . ports: - "8080:8080" environment: - DATABASE_URL=postgres://db:5432/app - REDIS_URL=redis://redis:6379 - SECRET_KEY=${SECRET_KEY} volumes: - ./config:/app/config depends_on: - db - redis

Enter fullscreen mode

Exit fullscreen mode

I open this file and I know everything:

No Slack archeology. No guessing. The documentation is the code.

The “But I Only Have One Container” Argument

People say: “I don’t need Compose, I only have one service.”

I disagree. Here’s why.

Reason 1: You’ll forget.

docker run commands grow. First it’s just -p 3000:3000. Then you add a volume. Then environment variables. Then a network. Six months later you have a 200-character command and zero memory of why each flag is there.

Reason 2: You’ll hate yourself.

Coming back to a project after a few months:

docker run -d --name app -p 3000:3000 -v $(pwd):/app -v /app/node_modules -e NODE_ENV=development -e DATABASE_URL=postgres://... -e REDIS_URL=redis://... -e JWT_SECRET=... --network app-network app:dev

vs.

docker compose up

Reason 3: Compose files are diffable.

When someone changes the configuration, I see it in the PR. Clear diff. Clear history. With shell commands? Good luck tracking what changed.

The Things That Bit Me

depends_on Lies to You

I wrote this and expected it to work:

services: db: image: postgres:15 app: build: . depends_on: - db

Enter fullscreen mode

Exit fullscreen mode

App crashed on startup. “Connection refused.”

depends_on doesn’t wait for the service to be ready. It waits for the container to start. Postgres takes a few seconds to initialize. My app tried to connect immediately. Boom.

The fix:

services: db: image: postgres:15 healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 2s timeout: 5s retries: 10 app: build: . depends_on: db: condition: service_healthy

Enter fullscreen mode

Exit fullscreen mode

Now Compose actually waits until Postgres responds to connections.

I’ve seen production deployments break because of this. People blame Docker. It’s not Docker’s fault – depends_on just doesn’t do what you think it does. (docs)

Anonymous Volumes Are a Trap (docs)

This looks innocent:

volumes: - /app/node_modules

Enter fullscreen mode

Exit fullscreen mode

It creates an anonymous volume. Works great until you need to:

Named volumes:

volumes: - node_modules:/app/node_modules volumes: node_modules:

Enter fullscreen mode

Exit fullscreen mode

Now it’s called projectname_node_modules. Searchable. Deletable. Manageable.

The Override File Nobody Reads About

Compose automatically merges docker-compose.override.yml if it exists (docs). No flags needed. Same with .env – it loads automatically, no env_file directive required (docs).

I use this pattern:

docker-compose.yml – production-like defaults, committed:

services: app: image: myapp:1.2.3 # pin versions in prod, never use :latest environment: - NODE_ENV=production

Enter fullscreen mode

Exit fullscreen mode

docker-compose.override.yml – dev settings, also committed:

services: app: build: . volumes: - .:/app environment: - NODE_ENV=development

Enter fullscreen mode

Exit fullscreen mode

Run docker compose up – you get development mode.

Run docker compose -f docker-compose.yml up – you get production mode.

No separate “dev” and “prod” compose files. No confusion about which one to use.

The Pattern That Changed Everything

Profiles (docs). Added in Compose 1.28, still underused.

Before profiles, I had this mess:

With profiles:

services: app: build: . # Always runs db: image: postgres:15 profiles: [dev] redis: image: redis:7 profiles: [dev] prometheus: image: prom/prometheus profiles: [monitoring] grafana: image: grafana/grafana profiles: [monitoring] test-db: image: postgres:15 profiles: [test] environment: - POSTGRES_DB=test

Enter fullscreen mode

Exit fullscreen mode

docker compose up # just app docker compose --profile dev up # app + db + redis docker compose --profile monitoring up # app + prometheus + grafana docker compose --profile dev --profile monitoring up # combine profiles

Enter fullscreen mode

Exit fullscreen mode

One file. Clear intent. No YAML soup.

The Commands Nobody Told Me About

docker compose up --wait (docs)

Most people run docker compose up -d and then manually check if everything started. There’s a better way:

docker compose up -d --wait

Enter fullscreen mode

Exit fullscreen mode

This returns control only when all services with healthchecks are healthy. Perfect for CI pipelines and scripts.

docker compose config (docs)

Your compose file uses variables and overrides. What’s the actual final config?

docker compose config

Enter fullscreen mode

Exit fullscreen mode

Shows the fully resolved YAML after all interpolation and merging. Saved me hours of “why isn’t this variable working” debugging.

docker compose watch (docs)

Volumes for hot reload are messy. They sync node_modules back and forth, cause permission issues, and slow things down.

Compose 2.22+ has a better way:

services: app: build: . develop: watch: - action: rebuild path: ./package.json - action: sync path: ./src target: /app/src

Enter fullscreen mode

Exit fullscreen mode

docker compose watch

Enter fullscreen mode

Exit fullscreen mode

File changes trigger rebuilds or syncs automatically. No bidirectional volume chaos.

Small Things That Matter

Environment variable defaults (docs)

services: app: ports: - "${PORT:-3000}:3000" environment: - LOG_LEVEL=${LOG_LEVEL:-info}

Enter fullscreen mode

Exit fullscreen mode

If the variable isn’t set, use the default. No more “variable not found” crashes.

init: true (docs)

services: app: init: true

Enter fullscreen mode

Exit fullscreen mode

One line. Proper signal handling. No zombie processes. Your containers will actually stop when you ask them to.

tmpfs for test databases (docs)

services: test-db: image: postgres:15 tmpfs: /var/lib/postgresql/data

Enter fullscreen mode

Exit fullscreen mode

Database runs entirely in memory. Tests run faster. Nothing persists between runs – which is exactly what you want for tests.

YAML anchors for DRY (docs)

x-logging: &default-logging logging: driver: json-file options: max-size: "10m" max-file: "3" services: app: <<: *default-logging build: . worker: <<: *default-logging build: ./worker

Enter fullscreen mode

Exit fullscreen mode

Define once, reuse everywhere. The x- prefix marks it as an extension (docs) – Compose ignores it, but YAML anchors still work.

The Compose File as Contract

Here’s how I think about it now.

A Dockerfile answers: “How do I build this?”

A docker-compose.yml answers: “How does this run in context?”

Context means:

Without this context, a Dockerfile is just a build recipe. With Compose, it becomes a runnable system.

The best infrastructure is the one you don’t have to explain. Write it down in a way that runs.

For further actions, you may consider blocking this person and/or reporting abuse

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Читайте також