A Compose file is often the most honest description of a system you have. It is what actually starts on a laptop, in CI and sometimes in production. It is also a wall of YAML, and a new teammate still has to ask which service talks to which.
This guide turns a Compose file into an architecture diagram you can trust: render the file Compose really runs, map each key to a diagram element, add the calls the file cannot express, and keep the drawing in step with the file. At the end we use Archboard's free tool to get the first draft in seconds.
The example: a small shop stack
Here is the compose.yaml we will draw. It is short, but it has the usual traps: a service on two networks, a dependency that only exists in a config file, and a dev-only tool.
name: shop
services:
web:
image: nginx:1.27
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- api
networks: [frontend]
api:
build: ./api
environment:
DATABASE_URL: postgres://shop:${POSTGRES_PASSWORD}@db:5432/shop
REDIS_URL: redis://cache:6379/0
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
networks: [frontend, backend]
worker:
build: ./api
command: ["node", "worker.js"]
environment:
DATABASE_URL: postgres://shop:${POSTGRES_PASSWORD}@db:5432/shop
REDIS_URL: redis://cache:6379/0
depends_on:
- db
deploy:
replicas: 2
networks: [backend]
db:
image: postgres:16
environment:
POSTGRES_USER: shop
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U shop"]
interval: 5s
networks: [backend]
cache:
image: redis:7
networks: [backend]
adminer:
image: adminer
ports:
- "8081:8080"
profiles: [debug]
networks: [backend]
volumes:
db-data:
networks:
frontend:
backend:
Step 1: Render the file Compose actually runs
Teams rarely run one file as written. An override file adds ports, a .env file fills in variables, and extends pulls fields from another service. Before you draw, ask Compose for the result:
docker compose config > rendered.yaml
docker compose config merges the override files, fills in variables and resolves extends, then prints the model Compose will run. Draw from that output, not from the file you happen to have open.
One warning: the output contains the real values of your variables, including anything secret in .env. Treat rendered.yaml like the .env file itself and do not commit or share it.
Step 2: Map Compose keys to diagram elements
Most of the diagram comes straight from the file:
| Compose key | Diagram element |
|---|---|
services.<name> |
One card per service, named after its key |
image / build |
On the card: the image tag, or the build context |
ports (8080:80) |
On the card, and on the edge from outside: :8080 → 80 |
deploy.replicas / scale |
A replica count on the card, not extra boxes |
networks |
A frame per network; each service sits in the frame of its first network |
Named volumes |
A volume node connected to the services that mount it |
secrets, configs |
Nodes connected to the services that use them, by name only |
depends_on |
Start-up order, written on the card or as a dashed edge |
profiles |
Optional services: leave them out or mark them as optional |
Two rules keep the picture honest. A service that joins two networks, like api, is the bridge between them, so place it at the edge of its first network's frame and let its connections cross into the other. And replicas are a number, not a stack of boxes: worker ×2 stays correct when you scale to five.
Step 3: Add the calls the file does not show
This is the step tools cannot do for you. depends_on is about start-up order: Compose starts db before api and, with condition: service_healthy, waits for the health check to pass. It says nothing about who calls whom at runtime.
The real calls hide in three places:
- Connection strings.
DATABASE_URLandREDIS_URLname hosts (db,cache) and ports. Each one is an edge. - Config files mounted into containers.
nginx.confcontainsproxy_pass http://api:3000;. That is the edge fromwebtoapi, and it lives outside the Compose file. - Code. A service can call another one without any line in Compose.
workerusesREDIS_URLbut has nodepends_onforcache. If Redis starts late, the worker fails on start-up, and the diagram is the first place that gap becomes visible.
A quick search finds most of them:
grep -rnE "://([^/@[:space:]]*@)?(api|web|db|cache)[:/]|proxy_pass" --include="*.conf" --include="*.yaml" --include="*.yml" --include="*.env" .
Then confirm with whoever owns each service. Draw runtime calls as solid edges labeled with protocol and port, and keep depends_on as a note or a dashed edge, so nobody mistakes start-up order for traffic.
Step 4: Decide what to leave out
A Compose file mixes the system with the tools around it. Usually you leave out:
- Dev-only services behind
profiles, such asadminer, mail catchers or debug proxies. - One-off jobs such as migrations or seeders, unless the diagram is about deployment.
- Bind mounts for source code and config files. Show a config file only through the dependency it carries, like the
proxy_passedge fromnginx.confhere. - Environment values. Names of variables can explain an edge; values belong in your secret store, not on a diagram.
Step 5: Lay it out
Put traffic left to right: the browser, then the published port, then the services behind it. Draw each network as a frame, so a call that crosses networks is easy to spot. Group the data stores together and put volumes under the service that mounts them. Label every edge with protocol and port, for example HTTP :3000 or SQL :5432.
Get the first draft with the free tool
You do not have to draw the cards by hand. Paste the file (or the rendered output; the tool does not read environment values) into the free Docker Compose to diagram tool. Viewing needs no account. Services become cards with their image, ports and replicas, networks become frames, named volumes, secrets and configs are connected to the services that use them, and depends_on becomes connections between services.
The tool draws only what the file says. It does not show runtime calls, it does not read environment values or env_file contents, it does not follow include, and for a service that uses extends it draws only the fields written in that service. That is why steps 1 and 3 still matter.
With a free account you open the result in Archboard as a new diagram, then add the runtime edges from step 3 and remove the noise from step 4. This is the shop stack after that pass:
Diagram contents
- Browser
- frontend
- backend
- web
- api
- cache
- db
- worker
- db-data
- HTTP :8080 → 80
- proxy_pass :3000
- SQL :5432
- RESP :6379
- jobs :6379
- /var/lib/postgresql/data
The cards come from the Docker pack, the browser from the Network pack, and the whole diagram stays within the Free plan's 15 diagram nodes. For another Docker layout, with containers, a registry and a load balancer, see the Docker runtime mesh example.
Keep it current
A diagram drawn from a Compose file goes stale the day someone adds a service. Three habits help:
- Add a line to the pull request template: "Adds a service, a network, a published port or a connection string? Update the diagram."
- Re-run
docker compose configduring reviews and compare the service list with the diagram. - Link the live diagram from the README instead of pasting a screenshot, so there is only one copy to update.
When the stack moves to Kubernetes, the same method applies to manifests; see From Kubernetes YAML to an architecture diagram.
FAQ
Does depends_on mean one service calls another?
No. It controls start-up order, and with condition: service_healthy it also waits for a health check. Runtime calls come from connection strings, config files and code.
Can I diagram several Compose files at once?
Render them first with docker compose config, which merges override files into one model. The free tool reads one file at a time.
Does the tool read my .env file?
No. Variables are not filled in: an image tag or port written with a variable, such as image: shop-api:${TAG}, is shown exactly as written, and values under environment and in env_file are not read.
Should I draw one box per container? Draw one card per service and write the replica count on it. Individual containers come and go; the service is what the team reasons about.
Will the diagram update when I change compose.yaml? No. It is a starting point. Keep it current with the habits above, or redraw from the tool and copy over your runtime edges.