# Diagrams as code
A series of 8 article(s), in reading order.
# Docker - Diagrams as code
> Generate stunning infrastructure and application diagrams directly from Python code using the diagrams library and a simple Docker command. Visualize your complex systems as code effortlessly.
---

**TL;DR**
This article shows how to render diagrams-as-code with the Python `diagrams` library via a ready-to-use Docker image (`gtramontina/diagrams`), piping a `.py` file into `docker run` to produce architecture diagrams (e.g. AWS/Azure/GCP icons) with zero local install. It closes with a broad roundup of other diagram-as-code tools: Mermaid, PlantUML, Graphviz, Kroki, DBML-renderer, Structurizr, and more.
But what a joy it is to be able to draw diagrams by just writing text. Some tools are better known than others, e.g. [Mermaid](https://mermaid-js.github.io/mermaid/) — which I've since automated in [Documentation as Code - Transform Your Infrastructure into Beautiful Diagrams with Python and Mermaid](/blog/docker-python-mermaid), and whose Graphviz cousin is covered in [Python - Generate flows using pydot](/blog/python-pydot).
Did you know ? Let's explore it using, of course, a Docker ready-to-use image.
## What comes out
Here is a diagram produced by this article, and nothing but text went in:

The command that produced it is a single line; the `.py` file is piped into a container and the `.png` lands in your current folder:
```bash
$ cat team.py | docker run -i --rm -v $(pwd):/out -u 1000:1000 gtramontina/diagrams:0.23.3
```
## Why it works
* The `.py` file is not executed on your machine: it's piped into the container, which contains Python, the `diagrams` library, Graphviz and the whole AWS/Azure/GCP/K8S icon sets.
* `-v $(pwd):/out` is what makes the generated image come back to your folder instead of dying with the container.
* `-u 1000:1000` runs the container as you, so the produced file belongs to your user and not to `root`.
> **Windows notation:**
>
> If you're working on Windows, replace `$(pwd)` with `%CD%`. And replace `cat` by `type`.
## The source
As always, for the demo, please start a Linux shell and run `mkdir -p /tmp/docker-diagrams && cd $_` to create a folder called `docker-diagrams` in your Linux temporary folder and jump in it.
Please create a new file called `team.py` with the content that produced the diagram above:
**team.py**
```python
from diagrams import Diagram
from diagrams.aws.compute import EC2
from diagrams.aws.database import RDS
from diagrams.aws.network import ELB
with Diagram("My Team", show=False, direction="TB"):
(
ELB("Olivier")
>> [EC2("Christophe"), EC2("Jason"), EC2("Niki"), EC2("Rudy"), EC2("Stijn")]
>> RDS("Work together")
)
```
Then run the conversion command shown here above.
*0.23.3 is the latest version available when writing this document. See to retrieve the latest one.*
Easy, right?
## A real architecture
Twenty lines of Python for a team chart is nice; here is the same exercise on an actual infrastructure:
**stateful.py**
```python
from diagrams import Cluster, Diagram
from diagrams.k8s.compute import Pod, StatefulSet
from diagrams.k8s.network import Service
from diagrams.k8s.storage import PV, PVC, StorageClass
with Diagram("Stateful Architecture", show=False):
with Cluster("Apps"):
svc = Service("svc")
sts = StatefulSet("sts")
apps = []
for _ in range(3):
pod = Pod("pod")
pvc = PVC("pvc")
pod - sts - pvc
apps.append(svc >> pod >> pvc)
apps << PV("pv") << StorageClass("sc")
```
And the resulting image:

Crazy, right? And all of this without installing anything!
> **More example:**
>
> Retrieve more samples at
The Docker image code base is here: , and you'll find plenty of other examples at .
## Icons and other tools (reference, skip this for now)
A tremendous list of icons/nodes is available on multiple pages at . See [OnPrem](https://diagrams.mingrammer.com/docs/nodes/onprem), [AWS](https://diagrams.mingrammer.com/docs/nodes/aws), [Azure](https://diagrams.mingrammer.com/docs/nodes/azure), [GCP](https://diagrams.mingrammer.com/docs/nodes/gcp), [IBM](https://diagrams.mingrammer.com/docs/nodes/ibm), [K8S](https://diagrams.mingrammer.com/docs/nodes/k8s) and also how to create our own (using local `.png` images): [Custom](https://diagrams.mingrammer.com/docs/nodes/custom).
And, since `diagrams` is far from being the only text-to-picture tool around, here is the list I keep coming back to:
* [DB Diagram](https://dbdiagram.io/home) *(see also [Drawdb-app - Render your database model as png, markdown, mermaid, ...](/blog/drawdb-app))*
* [DBML-renderer](https://github.com/softwaretechnik-berlin/dbml-renderer), dbml-renderer renders DBML files to SVG images
* [Graphviz](https://www.graphviz.org/), Graphviz is open source graph visualization software
* [JSON Crack](https://jsoncrack.com/), seamlessly visualize your JSON data instantly into graphs *(I've dedicated an article to it: [Rendering a JSON file as a mind map](/blog/json-crack))*
* [Kroki](https://kroki.io/), creates diagrams from textual descriptions
* [Mermaid](https://mermaid-js.github.io/mermaid/), its [live editor](https://mermaid.live/), the [preview addon for vscode](https://marketplace.visualstudio.com/items?itemName=vstirbu.vscode-mermaid-preview) and the [convert tool as a CLI tool](https://github.com/mermaid-js/mermaid-cli)
* [Nomnoml](https://www.nomnoml.com/), tool for drawing UML diagrams based on a simple syntax
* [Pikchr](https://pikchr.org/), Pikchr (pronounced "picture") is a PIC-like markup language for diagrams in technical documentation
* [Plantuml](https://github.com/plantuml/plantuml), generate diagrams from textual description
* [Sequence diagram](https://sequencediagram.org/) *(seems based on Mermaid)*
* [Structurizr](https://github.com/structurizr/dsl), a way to create Structurizr software architecture models based upon the C4 model using a textual domain specific language
* [svgbob](https://github.com/ivanceras/svgbob), convert your ascii diagram scribbles into happy little SVG
* [Vega](https://vega.github.io/vega/), A Visualization Grammar
* [yEd Graph Editor](https://www.yworks.com/products/yed), a graphical interface: you will need to drag & drop objects and resize them. It does not support text files like the other tools already mentioned here.
## Conclusion
Architecture diagrams that live in your repository, that you can diff and review like any other file, and that never require Python or Graphviz on your machine. The next time the infrastructure changes, you edit a line of text instead of dragging a box.
If you'd rather stay with a syntax you already know, [Documentation as Code - Transform Your Infrastructure into Beautiful Diagrams with Python and Mermaid](/blog/docker-python-mermaid) does the same trick with Mermaid.
---
# Build a mind map using Docker and Markdown
> Learn how to easily create a dynamic mind map using Markmap, Docker, and Markdown. Convert plain text into a beautiful, interactive HTML mind map.
---

**TL;DR**
This article shows how to turn a plain Markdown outline (headings and bullet lists) into an interactive, zoomable SVG mind map using Markmap, either via its online editor or with zero install through the `leopoul/markmap` Docker image (`docker run ... mindmap.md --output mindmap.html`).
As a markdown lover, I always enjoy finding a little tool that lets me write a text and have it converted into another format.
For this article, we're going to write, in plain text, a mind map, which means that our text will be converted into a mind map image.
## What comes out
Here is the mind map you'll get at the end of this article:

It's an SVG in an HTML page: branches can be folded and unfolded, and you can zoom in and out. And here is the single command that produced it, from a plain Markdown file:
```bash
docker run -it --rm -v ${PWD}:/project -w /project -u $(id -u):$(id -g) \
leopoul/markmap:1.0.0 mindmap.md --output mindmap.html
```
Nothing installed on the machine; the tool we're using is called `Markmap` and it stays in its container.
## And here is the text that produced it
**mindmap.md**
```markdown
# Social Media Uses
## Blogging
- Blogger
- Medium
- Joomla
## Social Network
### Common
- Facebook
### For developers
- Dev.to
- Daily.dev
- Github
## Photo Sharing
- Flicker
- Pinterest
```
Headings become branches, bullet lists become leaves. That's the entire syntax; you already know it.
## Doing it on your machine
For the demo, please start a Linux shell and run `mkdir -p /tmp/markmap && cd $_` to create a folder called `markmap` in your Linux temporary folder and jump in it.
Please create a new file called `mindmap.md` with the markdown content about *Social Media Uses* provided just here above. You should have this:
```bash
$ ls -alh
total 920K
drwxr-xr-x 2 christophe christophe 4.0K Dec 16 14:05 .
drwxrwxrwt 19 christophe christophe 908K Dec 16 14:05 ..
-rw-r--r-- 1 christophe christophe 198 Dec 16 14:05 mindmap.md
```
And now run the `docker run` command shown at the top of this article to convert the markdown document into an HTML page. The image is automatically created as a SVG content in the `.html` file:
```bash
$ ls -alh
total 920K
drwxr-xr-x 2 christophe christophe 4.0K Dec 16 14:05 .
drwxrwxrwt 19 christophe christophe 908K Dec 16 14:05 ..
-rw-r--r-- 1 christophe christophe 2.2K Dec 16 14:10 mindmap.html
-rw-r--r-- 1 christophe christophe 198 Dec 16 14:05 mindmap.md
```
Open `mindmap.html` in your browser and you'll get the map displayed using the full-screen width.
> **WSL User:**
>
> If you're running under Windows and WSL2, to open the `mindmap.html` file, one way is to run `explorer.exe .` in your Linux console (see [this article](/blog/wsl-windows-explorer) to learn more). Windows Explorer will be started then just double-click on the `mindmap.html` file.
## Without Docker, in your browser
`Markmap` can also be used online: see the demo and the editor on . Copy/paste the markdown above into the [editor](https://markmap.js.org/repl) to see it in action, without creating a single file.
## Go further
You can add some configuration items in your markdown document, see to get a list of all supported JSON options.
## Conclusion
A mind map you can version, diff and edit in any text editor, produced by one `docker run` — and nothing to uninstall afterwards.
Two follow-ups: [Getting a more attractive mindmap with Markmap and Quarto](/blog/vscode-docker-markmap), which polishes the rendering, and [Rendering a JSON file as a mind map](/blog/json-crack) when the source is a JSON file rather than Markdown.
---
# Rendering a JSON file as a mind map
> Transform complex JSON data into clear, interactive mind maps with JSON Crack. Discover how this powerful tool simplifies data visualization and enhances data understanding.
---

**TL;DR**
This article presents JSON Crack, a free online tool that renders JSON content as an interactive mind map/diagram instead of raw nested text, making complex structures much easier to read at a glance. It shows two example JSON files rendered as visual trees and mentions the free version's line-count limitation along with a VSCode extension alternative.
>
I really like the idea of not having to draw (actually, that is just because I am really bad at it) a diagram, a flow chart, a graphic representation of something that ... can be written.
[JSON Crack](https://jsoncrack.com/editor) is one of the tools in my toolkit when I want to draw something that can be written in JSON. *It sits next to [Markmap](/blog/docker-mindmap) (same idea, but from Markdown) and [the diagram-as-code tools](/blog/docker-diagram-as-code) in general.*
*When you just need to read the JSON rather than visualize it, [JSON - Online linter](/blog/json-lint) and [`jq`](/blog/linux-jq) do the job.*
Let's take an example found randomly on the net [superheroes](https://medium.com/@Goldzila/superheroes-of-data-exploring-xml-json-and-binary-formats-through-the-lens-of-marvel-characters-3754f2691cdc). How can we represent Spider-Man as a JSON object? Below an attempt:
**superheroes.json**
```json
{
"superhero": "Spider-Man",
"realName": "Peter Parker",
"abilities": [
"Wall-Crawling",
"Spider-Sense",
"Super Strength"
],
"equipment": {
"webShooters": "True",
"spiderTracers": "True"
}
}
```
Isn't it nicer in visual form?

Another example (found [here](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/JSON)):
**superheroes.json**
```json
{
"squadName": "Super hero squad",
"homeTown": "Metro City",
"formed": 2016,
"secretBase": "Super tower",
"active": true,
"members": [
{
"name": "Molecule Man",
"age": 29,
"secretIdentity": "Dan Jukes",
"powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
},
{
"name": "Madame Uppercut",
"age": 39,
"secretIdentity": "Jane Wilson",
"powers": [
"Million tonne punch",
"Damage resistance",
"Superhuman reflexes"
]
},
{
"name": "Eternal Flame",
"age": 1000000,
"secretIdentity": "Unknown",
"powers": [
"Immortality",
"Heat Immunity",
"Inferno",
"Teleportation",
"Interdimensional travel"
]
}
]
}
```

The advantage of the image is that it is much clearer: we can see straight away that the squad is made up of three members and that the strongest (and oldest) is *Eternal Flame*, who has five powers.
> **Note:**
>
> JSON Crack has some limitations for the free version like the number of lines in your JSON content. You can also install an [add-on for vscode](https://marketplace.visualstudio.com/items?itemName=AykutSarac.jsoncrack-vscode).
Other JSON tools worth knowing about: [generating fake JSON data](/blog/json-faker) and [linting/validating your JSON files](/blog/json-lint).
---
# How to generate a graph from compose.yaml
> Instantly visualize your Docker Compose architecture. Use `compose-viz` to generate a clear graph of services, dependencies, and exposed ports from your `compose.yaml` file.
---

**TL;DR**
This article shows how to visualize a `compose.yaml` file as a dependency graph using the `compose-viz` Docker image, producing an image (PNG, SVG, and other formats via `--format`) that shows services, their dependencies, and exposed ports at a glance — useful for understanding large multi-service Docker architectures. It also mentions `docker compose config` to resolve and merge multiple `.yml` files/variables before visualizing.
Imagine you have a very big `compose.yaml` file with a lot of services and dependencies.
You also have multiple exposed ports.
You'd like to have an at-a-glance view of services, dependencies and open ports. Is this possible? Yes! does the magic for us.
Here's what `compose-viz` generates from a Joomla `compose.yaml`:

> **How to interpret the image?:**
>
> We can see that the port `8080` is exposed to the computer. That port is, in fact, port `80` of the `joomla` container. And, too, we see that `joomla` has a dependency with the `joomladb` container. Easy no?
## Reproduce it yourself
For the demo, please start a Linux shell and run `mkdir -p /tmp/compose-viz && cd $_` to create a folder called `compose-viz` in your Linux temporary folder and jump in it.
Please create a new file called `compose.yaml` with this **simplified** content:
**compose.yaml**
```yaml
services:
joomla:
image: joomla
ports:
- 8080:80
depends_on:
- joomladb
joomladb:
image: postgres:16.0-alpine
```
Let the magic happen and run `docker run --rm -it -u $(id -u):$(id -g) -v $(pwd):/in wst24365888/compose-viz compose.yaml` — you'll get the image shown above.
> **See the --format flag:**
>
> By adding `--format svg`, you'll get an SVG instead of a PNG. The list of supported output formats is huge: `png|dot|jpeg|json|svg|bmp|canon|cmap|cmapx|cmapx_np|dot_json|emf|emfplus|eps|fig|gif|gv|imap|imap_np|ismap|jpe|jpg|json0|metafile|mp|pdf|pic|plain|plain-ext|pov|ps|ps2|tif|tiff|tk|vml|xdot|xdot1.2|xdot1.4|xdot_json`.
## A more complex example
Replace the content of the `compose.yaml` with this one:
**compose.yaml**
```yaml
services:
redis:
image: redis:alpine
ports:
- "6379"
networks:
- frontend
deploy:
replicas: 2
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
db:
image: postgres:9.4
volumes:
- db-data:/var/lib/postgresql/data
networks:
- backend
deploy:
placement:
constraints: [ node.role == manager ]
vote:
image: dockersamples/examplevotingapp_vote:before
ports:
- 5000:80
networks:
- frontend
depends_on:
- redis
deploy:
replicas: 2
update_config:
parallelism: 2
restart_policy:
condition: on-failure
result:
image: dockersamples/examplevotingapp_result:before
ports:
- 5001:80
networks:
- backend
depends_on:
- db
deploy:
replicas: 1
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
worker:
image: dockersamples/examplevotingapp_worker
networks:
- frontend
- backend
deploy:
mode: replicated
replicas: 1
labels: [ APP=VOTING ]
restart_policy:
condition: on-failure
delay: 10s
max_attempts: 3
window: 120s
placement:
constraints: [ node.role == manager ]
visualizer:
image: dockersamples/visualizer
ports:
- "8080:8080"
stop_grace_period: 1m30s
volumes:
- /var/run/docker.sock:/var/run/docker.sock
deploy:
placement:
constraints: [ node.role == manager ]
networks:
frontend:
backend:
volumes:
db-data:
```
And the image here below will be generated by the tool:

Here, we can see:
* We've four exposed ports: `5001`, `5000`, `6379` and `8080`.
* Port `8080` is linked to a `visualizer` web service; that service is using a volume pointing to `/var/run/docker.sock`
* Ports `5000` and `5001` are referring to services called `vote` and `result`. Since the internal port is port `80`, we know that these ports are web interfaces.
* The `vote` service (port `5000`) is using a `redis` service; accessible using port `6379`.
* The `result` service (port `5001`) is linked to a Postgres service called `db` and having persistent data (since we have a Docker volume called `db-data`).
* And so on.
Such a visualization tool greatly simplifies the understanding of a Docker architecture.
*`compose-viz` gives you one fixed rendering. If you want to control exactly what the diagram shows, [Documentation as Code - Transform Your Infrastructure into Beautiful Diagrams with Python and Mermaid](/blog/docker-python-mermaid) parses the same `compose.yaml` with a Python script you own, and [Docker - Diagrams as code](/blog/docker-diagram-as-code) lists a dozen other diagram-as-code tools.*
## Docker config
If like me you're using a lot of `.yml` files by running Docker and/or using environment variables in it; just run `docker compose config` to ask Docker to *render* (on the screen only) what it calls *Parse, resolve and render compose file in canonical format* i.e. it will merge all your `.yml` in one string and resolve variables.
Copy/paste the screen output in a temporary file and use this tool on that file.
---
# Drawdb-app - Render your database model as png, markdown, mermaid, ...
> Easily visualize and document your database model with DrawDB-app. Import SQL files, generate diagrams, and export to PNG, Markdown, Mermaid, and more.
---

**TL;DR**
This article introduces drawdb-app, a free tool that renders a visual ER diagram from an imported `.sql` file (or built from scratch) and exports it as PNG, JSON, Mermaid, or auto-generated Markdown documentation (tables, columns, relationships) — also self-hostable via Docker (`xinsodev/drawdb`).
In my RSS feed recently, I came across another great application that lets you create a database model from scratch, i.e. create tables one by one and create the links between them.
However, there's another option that I find really nice: you can get a visual of the tables and relationships of any existing application in a few seconds from an .sql file that you import into drawdb-app. *Getting that `.sql` dump out of a running container is a one-click job with [Adminer, pgadmin or phpmyadmin](/blog/docker-adminer-pgadmin-phpmyadmin).*
Let's take a quick look at how it works.
By opening the website, the first thing you'll be prompted to do is to choose your target database format. For instance, let's choose PostgreSQL (Situation: *I'm coding a Laravel application using PostgreSQL and I want to see the relations of my DB in a nice pic*).
I think the most common use I could personally make of it is to load an SQL file. To do this, just click on the `File` menu, then choose `Import from SQL` and, finally, retrieve a previously created .sql file from your hard drive.
## Create and import a dummy sql file
If you don't have such a file, I suggest copying one from . Below, you'll find PostgreSQL content taken from that site ([direct link](https://www.sqltutorial.org/wp-content/uploads/2020/04/postgresql.txt)) — or you can grab any valid `.sql` file you find on the Internet.
**create\_db.sql**
```sql
CREATE TABLE regions (
region_id SERIAL PRIMARY KEY,
region_name CHARACTER VARYING (25)
);
CREATE TABLE countries (
country_id CHARACTER (2) PRIMARY KEY,
country_name CHARACTER VARYING (40),
region_id INTEGER NOT NULL,
FOREIGN KEY (region_id) REFERENCES regions (region_id) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE TABLE locations (
location_id SERIAL PRIMARY KEY,
street_address CHARACTER VARYING (40),
postal_code CHARACTER VARYING (12),
city CHARACTER VARYING (30) NOT NULL,
state_province CHARACTER VARYING (25),
country_id CHARACTER (2) NOT NULL,
FOREIGN KEY (country_id) REFERENCES countries (country_id) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE TABLE departments (
department_id SERIAL PRIMARY KEY,
department_name CHARACTER VARYING (30) NOT NULL,
location_id INTEGER,
FOREIGN KEY (location_id) REFERENCES locations (location_id) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE TABLE jobs (
job_id SERIAL PRIMARY KEY,
job_title CHARACTER VARYING (35) NOT NULL,
min_salary NUMERIC (8, 2),
max_salary NUMERIC (8, 2)
);
CREATE TABLE employees (
employee_id SERIAL PRIMARY KEY,
first_name CHARACTER VARYING (20),
last_name CHARACTER VARYING (25) NOT NULL,
email CHARACTER VARYING (100) NOT NULL,
phone_number CHARACTER VARYING (20),
hire_date DATE NOT NULL,
job_id INTEGER NOT NULL,
salary NUMERIC (8, 2) NOT NULL,
manager_id INTEGER,
department_id INTEGER,
FOREIGN KEY (job_id) REFERENCES jobs (job_id) ON UPDATE CASCADE ON DELETE CASCADE,
FOREIGN KEY (department_id) REFERENCES departments (department_id) ON UPDATE CASCADE ON DELETE CASCADE,
FOREIGN KEY (manager_id) REFERENCES employees (employee_id) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE TABLE dependents (
dependent_id SERIAL PRIMARY KEY,
first_name CHARACTER VARYING (50) NOT NULL,
last_name CHARACTER VARYING (50) NOT NULL,
relationship CHARACTER VARYING (25) NOT NULL,
employee_id INTEGER NOT NULL,
FOREIGN KEY (employee_id) REFERENCES employees (employee_id) ON DELETE CASCADE ON UPDATE CASCADE
);
```
So just copy/paste this SQL in Notepad; save it on your disk and go back to ; `File` -> `Import from SQL`.
And let the magic happen:

> **The image here above has been done using the `File` -> `Export as` feature.:**
### You can self-host the application thanks to Docker if you want
By visiting , you'll find a few Docker images for drawdb-app. For instance .
To run it, just start `docker run --name some-drawdb -p 81:80 -d xinsodev/drawdb` and access the interface on `http://localhost:81`.
## Playing with exportation feature
From now, take time to check export features; click on the `File` menu again and click on `Export as`.

It's really cool to export a SQL file so easily to JSON but it's even cooler to export as markdown. *That Mermaid export plugs straight into the pipeline described in [Documentation as Code - Transform Your Infrastructure into Beautiful Diagrams with Python and Mermaid](/blog/docker-python-mermaid), and drawDB joins the list of tools in [Docker - Diagrams as code](/blog/docker-diagram-as-code).*
## Under the Hood (skip this if you just want to use it)
> **Auto-generated content here below:**
>
> The rest of this article was generated by drawDB.app; a really useful tool to help developers document their database! Below are two representative tables — `employees` (the busiest one, with three foreign keys) and `regions` (the simplest) — out of the seven the full export produced, plus the relationships between all of them.
### Table structure
#### employees
| Name | Type | Settings | References | Note |
| ------------------ | ------------ | --------------- | ----------------------------- | ---- |
| **employee\_id** | SERIAL | 🔑 PK, not null | | |
| **first\_name** | BLOB | not null | | |
| **last\_name** | BLOB | not null | | |
| **email** | BLOB | not null | | |
| **phone\_number** | BLOB | not null | | |
| **hire\_date** | DATE | not null | | |
| **job\_id** | INTEGER | not null | employees\_job\_id\_fk | |
| **salary** | NUMERIC(8,2) | not null | | |
| **manager\_id** | INTEGER | not null | | |
| **department\_id** | INTEGER | not null | employees\_department\_id\_fk | |
#### regions
| Name | Type | Settings | References | Note |
| ---------------- | ------ | --------------- | ---------- | ---- |
| **region\_id** | SERIAL | 🔑 PK, not null | | |
| **region\_name** | BLOB | not null | | |
### Relationships
* **countries to regions**: many\_to\_one
* **locations to countries**: many\_to\_one
* **departments to locations**: many\_to\_one
* **employees to jobs**: many\_to\_one
* **employees to departments**: many\_to\_one
* **dependents to employees**: many\_to\_one
## Conclusion
Import a `.sql` file, get a rendered ER diagram back in a few seconds, and export it as PNG, JSON, Mermaid, or ready-made Markdown documentation — all without installing anything, or self-hosted via the `xinsodev/drawdb` Docker image if you'd rather keep it in-house. The Mermaid export slots straight into [Documentation as Code - Transform Your Infrastructure into Beautiful Diagrams with Python and Mermaid](/blog/docker-python-mermaid), and drawDB itself is one more entry in the toolbox from [Docker - Diagrams as code](/blog/docker-diagram-as-code).
---
# Python - Generate flows using pydot
> Use Python and the pydot library to easily generate diagrams-as-code like ETL flowcharts, class diagrams, and decision trees. Includes Docker setup and code examples.
---

**TL;DR**
This article introduces Pydot, a Python diagram-as-code library that renders diagrams (ETL flows, class diagrams, data flows, decision trees) from Python code into images. Using a small Docker image, a `.py` script describing the diagram is run with `docker run ... python etl.py` to produce a PNG, with further examples sourced from the Graphviz documentation.
[Pydot](https://github.com/pydot/pydot) is a **diagram-as-code** generator, i.e. you write lines of code and, thanks to a magic process, you can render the code as an image.
*This blog has two other articles in the same family: [Docker - Diagrams as code](/blog/docker-diagram-as-code), using the `diagrams` library and its cloud-provider icons, and [Documentation as Code - Transform Your Infrastructure into Beautiful Diagrams with Python and Mermaid](/blog/docker-python-mermaid), where the diagram is generated **from** your `compose.yaml` instead of hand-written.*
For guys just like me who are terrible at visuals, this is gold.
Let's say you have to describe an IT process such as ETL (data is loaded, one or other transformation rule is applied and the result is loaded into a database, for example); you can easily imagine drawing some rectangles using a tool like (formerly known as draw\.io), or use a smarter way to do it.
Let's see how in this article.
Pydot can generate an image like the one below:

First, we'll need a Python Docker container. Please create a file called `Dockerfile` with this content:
**Dockerfile**
```docker
FROM python:3.13-slim
# Prevents Python from writing pyc files.
ENV PYTHONDONTWRITEBYTECODE=1
# hadolint ignore=DL3008
RUN set -e -x \
&& apt-get update -yqq \
&& apt-get install -y --no-install-recommends graphviz \
&& apt-get clean \
&& rm -rf /tmp/* /var/lib/apt/lists/*
# hadolint ignore=DL3013
RUN pip install --no-cache-dir pydot
```
And create the image by running `docker build --tag pydot .`.
We don't need more except ... our flow.
To build an ETL image like the one above, please create a new file called f.i. `etl.py` with this content:
**etl.py**
```python
graph = pydot.Dot(graph_type='digraph', rankdir='TB')
node_csv = pydot.Node('CSV', shape='box', style='filled', fillcolor='lightblue')
node_json = pydot.Node('JSON', shape='box', style='filled', fillcolor='lightblue')
node_extract = pydot.Node('Extract', shape='box')
node_clean = pydot.Node('Clean', shape='box')
node_transform = pydot.Node('Transform', shape='box')
node_load = pydot.Node('Load', shape='box', style='filled', fillcolor='lightgreen')
graph.add_edge(pydot.Edge(node_csv, node_extract))
graph.add_edge(pydot.Edge(node_json, node_extract))
graph.add_edge(pydot.Edge(node_extract, node_clean))
graph.add_edge(pydot.Edge(node_clean, node_transform))
graph.add_edge(pydot.Edge(node_transform, node_load))
node_enrich = pydot.Node('Get more data', shape='box')
graph.add_edge(pydot.Edge(node_transform, node_enrich))
graph.add_edge(pydot.Edge(node_enrich, node_load))
graph.write_png('etl.png')
```
And, now, the easy part, render the Python `etl.py` file as an image by running `docker run --rm -it -v "${PWD}":/diagram -w /diagram pydot python etl.py`.
And bingo, you've now an image called `etl.png` in your directory.
## Some other examples
Search on the Internet for some examples and you'll find f.i. this site: .
You'll find there Python examples and, too, the rendered image.
### OOP classes
**class\_diagram.py**
```python
graph = pydot.Dot(graph_type='digraph')
# Add nodes
node_person = pydot.Node('Person', shape='box')
node_student = pydot.Node('Student', shape='box')
node_teacher = pydot.Node('Teacher', shape='box')
# Add nodes to the graph
graph.add_node(node_person)
graph.add_node(node_student)
graph.add_node(node_teacher)
# Add edges (inheritance)
graph.add_edge(pydot.Edge(node_person, node_student))
graph.add_edge(pydot.Edge(node_person, node_teacher))
# Render the graph
graph.write_png('class_diagram.png')
```

### Data flow
**data\_flow\.py**
```python
graph = pydot.Dot(graph_type='digraph')
# Data sources
source1 = pydot.Node('Source 1', shape='box')
source2 = pydot.Node('Source 2', shape='box')
graph.add_node(source1)
graph.add_node(source2)
# Process nodes
process1 = pydot.Node('Process 1', shape='oval')
process2 = pydot.Node('Process 2', shape='oval')
graph.add_node(process1)
graph.add_node(process2)
# Data store
data_store = pydot.Node('Data Store', shape='cylinder')
graph.add_node(data_store)
# Edges representing data flow
graph.add_edge(pydot.Edge(source1, process1))
graph.add_edge(pydot.Edge(source2, process1))
graph.add_edge(pydot.Edge(process1, process2))
graph.add_edge(pydot.Edge(process1, data_store))
graph.add_edge(pydot.Edge(data_store, process2))
# Render the graph
graph.write_png('data_flow_diagram.png')
```

### Decision flow
**decision\_flow\.py**
```python
graph = pydot.Dot(graph_type='digraph')
# Add nodes
node_start = pydot.Node('Start', shape='oval')
node_process1 = pydot.Node('Process 1', shape='box')
node_decision = pydot.Node('Decision', shape='diamond')
node_process2 = pydot.Node('Process 2', shape='box')
node_end = pydot.Node('End', shape='oval')
# Add edges
graph.add_edge(pydot.Edge(node_start, node_process1))
graph.add_edge(pydot.Edge(node_process1, node_decision))
graph.add_edge(pydot.Edge(node_decision, node_process2, label='Yes'))
graph.add_edge(pydot.Edge(node_decision, node_end, label='No'))
graph.add_edge(pydot.Edge(node_process2, node_end))
# Write the graph to a file
graph.write_png('flowchart.png')
```

### Decision tree
**decision\_tree.py**
```python
graph = pydot.Dot(graph_type='digraph')
# Add nodes
node_root = pydot.Node('Root', shape='box')
node_left = pydot.Node('Left Child', shape='box')
node_right = pydot.Node('Right Child', shape='box')
node_left_left = pydot.Node('Left-Left Grandchild', shape='ellipse')
node_left_right = pydot.Node('Left-Right Grandchild', shape='ellipse')
node_right_left = pydot.Node('Right-Left Grandchild', shape='ellipse')
node_right_right = pydot.Node('Right-Right Grandchild', shape='ellipse')
# Add nodes to the graph
graph.add_node(node_root)
graph.add_node(node_left)
graph.add_node(node_right)
graph.add_node(node_left_left)
graph.add_node(node_left_right)
graph.add_node(node_right_left)
graph.add_node(node_right_right)
# Add edges
graph.add_edge(pydot.Edge(node_root, node_left, label='Feature 1 < 5'))
graph.add_edge(pydot.Edge(node_root, node_right, label='Feature 1 >= 5'))
graph.add_edge(pydot.Edge(node_left, node_left_left, label='Feature 2 < 3'))
graph.add_edge(pydot.Edge(node_left, node_left_right, label='Feature 2 >= 3'))
graph.add_edge(pydot.Edge(node_right, node_right_left, label='Feature 3 < 10'))
graph.add_edge(pydot.Edge(node_right, node_right_right, label='Feature 3 >= 10'))
# Render the graph
graph.write_png('decision_tree.png')
```

---
# Getting a more attractive mindmap with Markmap and Quarto
> Generate attractive mindmaps from Markdown using Markmap and customize them in VSCode. Render the final, beautiful HTML result with Quarto and Docker.
---

**TL;DR**
This article provides a step-by-step guide on how to create attractive, interactive mind maps from simple Markdown files. You'll learn how to use the Markmap VS Code extension to instantly visualize your Markdown lists as mind maps directly within your editor. The post also covers how to customize the appearance of your mind map using VS Code settings. Finally, it demonstrates how to use Quarto, via a simple Docker-based shell script, to render your mind map into a polished, standalone HTML page, without needing to install any tools locally.
In 2023, I wrote this [Build a mind map using Docker and Markdown](/blog/docker-mindmap) blog post about how to render a Markdown document as a mindmap.
This time, I would like to go one step further and make it more attractive.
*Two other ways to turn text into a diagram on this blog: [generating Mermaid from a Python script](/blog/docker-python-mermaid) and [rendering a JSON file as a mind map](/blog/json-crack).*
Here's where we're headed — the same plain Markdown list, rendered as a polished, standalone HTML mindmap with Quarto:

Let's start by the beginning: please start VSCode, create a new file. Because we'll do a little more than this, let's create a new folder: `mkdir -p /tmp/markmap && cd $_`.
Please create a Markdown file there f.i. `overview.qmd`. The `.qmd` extension is just to tell *This is a Markdown file and we plan to use it with Quarto*; nothing really special here.
**overview\.qmd**
```qmd
# 📚 Project Overview
## 🚀 Goals
- ✅ **Launch MVP**
- 🌱 Collect ==user feedback==
- 📈 Iterate every 2 weeks
## 🧰 Tech Stack
- `Next.js` + `Tailwind`
- 🧠 AI via OpenAI
- 🐘 PostgreSQL DB
## 🔁 Feedback Loop
- 📞 Customer interviews
- 📝 Surveys
- 📊 Usage metrics
## 📅 Timeline
- Q1 → Prototyping
- Q2 → MVP 🚢
- Q3 → Growth 📈
```
> **Note:**
>
> The content of the `overview.qmd` file here above has been generated by AI.
So, we'll have this:

## Render the markmap online
Jump to and paste the Markdown content to immediately get a first rendering:

Ok, it works but not really impressive.
## Installing the VSCode extension
There is a Markmap extension on the marketplace; jump to to install it.
Once installed in VSCode, press `CTRL`+`SHIFT`+`P` to open the *Command Palette* and then search for `Markmap`, please select `Markmap: open as markmap` and press `Enter`.
You'll get almost the same output as the online version:

### Configuring the rendering
By creating a `.vscode/settings.json` file, you can specify some CSS and default options i.e. by configuring the Markmap-vscode extension.
Please create the `.vscode/settings.json` file with this content:
**.vscode/settings.json**
```json
{
"markmap.customCSS": ".markmap-link,\nline {\n\tstroke: #505050;\n\tstroke-width: 1.5px;\n}\n.markmap-node > circle {\n\tstroke: #202020;\n\tstroke-width: 1.5px;\n\tr: 6px;\n\tfill: #242424;\n}\n\n.markmap-foreign a {\n\ttext-decoration: none;\n\tcolor: #bbbbbb !important;\n}\n\n.markmap-highlight rect {\n\t/* fill: #303030 !important; */\n\tfill: transparent !important;\n\ttransform: scale(0.84, 0.7) !important;\n\ttransform-origin: center !important;\n\ttransform-box: fill-box;\n\ttransform-origin: center;\n}",
"markmap.defaultOptions": "{\n \"color\": \"function(d, i) { return d3.interpolateRainbow(i / 10); }\",\n \"nodeFont\": \"20px 'Segoe UI', 'Helvetica Neue', sans-serif\",\n \"spacingVertical\": 40,\n \"spacingHorizontal\": 140,\n \"initialExpandLevel\": -1,\n \"duration\": 1000,\n \"paddingX\": 10,\n \"paddingY\": 10\n}"
}
```
Save the file, go back to the `overview.qmd` file and if needed, run `Markmap: open as markmap` again.
The preview will be better:

> **Read the doc:**
>
> The list of existing options is documented on the official site:
## Rendering as an HTML page using Quarto
The last step for this article, let's render our Markdown content as a nice HTML page; this by using Quarto.
Let's create a new file: `render.sh` with the following content:
**render.sh**
```bash
#!/usr/bin/env bash
set -euo pipefail
mkdir -p .build/.quarto_deno_cache_data/deno
chmod -R 777 .build/.quarto_deno_cache_data
# shellcheck disable=SC2046 # id -u/-g never contain whitespace, safe unquoted
docker run -it --rm \
-u $(id -u):$(id -g) \
-v ".:/public" \
-w /public \
-v "./.build/.quarto_deno_cache_data:/container_cache" \
-e DENO_DIR="/container_cache/deno" \
-e QUARTO_CACHE_DIR="/container_cache/quarto" \
-e HOME="/public" \
ghcr.io/quarto-dev/quarto \
quarto render overview.qmd --no-cache --output-dir .build --quiet
( cd .build && explorer.exe overview.html )
```
Please make this file executable by running `chmod +x render.sh`.
Now, time to call it: in your console, please run `./render.sh` to execute the script.
The very first time, it'll be longer because Docker has to download the Quarto Docker image (`ghcr.io/quarto-dev/quarto`).
The script will call Quarto, do some initializations and convert the `overview.qmd` file as an HTML page that will be stored in a newly `.build` folder.
So, after you have fired the script, you'll then have this structure in your project:

The nice thing is: Quarto has rendered your Markdown content as a beautiful page — the same result already shown at the top of this article, which was the objective of this blog post.
## Conclusion
It's very easy to write a MindMap-type structure and generate either an image or a web page. We could go even further with Quarto, because a web page can be customized using CSS.
And, because it's a web page, you can easily copy and paste the rendering into an email, Word document or anything else and get a pretty cool visual for what was originally just a simple list.
---
# Documentation as Code - Transform Your Infrastructure into Beautiful Diagrams with Python and Mermaid
> Automate your Documentation as Code by building a Dockerized Python and Mermaid CLI stack to generate dynamic architecture diagrams directly from your codebase.
---

**TL;DR**Manual documentation gets outdated the second your code changes. To solve this, you can embrace Documentation as Code by building a custom Docker image equipped with Python and the Mermaid CLI to automatically generate your visual assets. By writing straightforward Python scripts that parse your compose.yaml files or scan your project directories, you can dynamically output Mermaid pseudocode and render it directly into clean, up-to-date PNG diagrams (like mindmaps and pie charts) using a single command.
I like documentation, and what is more inefficient than writing docs for things that can be self-documented?
As you might know, I love Docker and I do almost everything in a dockerized way. So, in every one of my projects, there is a `compose.yaml` file. If I want to document it for my colleagues, I could start writing: *There are three services, one is for PHP, one for Nginx, one for PostgreSQL. That one is using a volume to make data persistent on disk and blah blah blah. The Nginx service is publishing on port xxx and the database is accessible using port yyy.* I can do that, for sure, but isn't there a better way to do this? What if I add a new service or update a port number? My documentation would already be outdated, unless...
Let's see how to automate the documentation using Docker, Python and Mermaid.
**Quick Jump:** [See it in Action](#the-result) | [All needed files](#all-needed-files)
## The Result
Point the pipeline built in this article at a real `compose.yaml`, and one command produces this:

This image has been generated entirely from the `compose.yaml` file — no manual drawing, no diagramming tool. Change the YAML, run the command again, and the diagram updates itself.
## Why Mermaid
> **Pseudocode in, image out:**
>
> [Mermaid](https://mermaid.js.org/) turns a small block of text — its own *pseudocode* — into a rendered diagram. Look at this [example](https://mermaid.live/edit#pako:eNpFjctqwzAQRX9FzNoNtmzJtrYtfUALhWbT4o1ijR0RWRMUmTYN-fcqCU3uauZw7swBejIICqKd0FmPnWcp0UaH7NnuIoU9o4F9UG-1Y29orGbvTseBwnRxeZ5zptir9Rs0L_4KqwQfdY8ros0FnqPYE9Ho8OqJhD5pXs6rG5OJLb9tjBgggzFYAyqGGTOYMEz6tMLhZHcQ1zhhByqNBgc9u9hB54-pttX-i2j6bwaaxzWoQbtd2uat0REfrB6DvinoDYZ7mn0EVYi6Ph8BdYAfUHdFVS7aVuZlK9uiraumzGAPipcLIaXkKTWvmrqRxwx-z4-LBW8L0XDR5GVTiroSxz-rtW0b): on the left, the pseudocode — readable at a glance; on the right, the rendered image.

Go to [Mermaid Live Editor](https://mermaid.live/edit) and play with the *Sample Diagrams* bottom left to discover all the tool has to offer. A Python script can generate that same pseudocode from anything readable — a `compose.yaml`, a folder tree, whatever data you already have — and the Mermaid CLI renders it into a transparent PNG.
## Installation — Let's create the Docker image
The Dockerfile we'll use is this one. It's quite simple.
**Dockerfile**
```docker
FROM node:20-alpine
RUN apk add --no-cache \
chromium \
nss \
freetype \
harfbuzz \
ca-certificates \
ttf-freefont \
font-noto-emoji \
python3 \
py3-pip \
py3-yaml
RUN fc-cache -fv
RUN npm install -g @mermaid-js/mermaid-cli
RUN mkdir -p /home/node/.cache/puppeteer && \
chmod -R 777 /home/node
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
ENV HOME=/home/node
WORKDIR /app
```
To build this image run `docker build -t yourself/docker-python-mermaid .` in your terminal:
```bash
$ docker build -t yourself/docker-python-mermaid .
```
The final image size will be around 2GB.

## Prepare Mermaid rendering
We need to have two external files for the rendering.
The first one will configure Puppeteer:
**puppeteer-config.json**
```json
{
"args": [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-accelerated-2d-canvas",
"--no-first-run",
"--no-zygote",
"--disable-gpu"
]
}
```
And the second one will allow us to use our own formatting (font, colors, ...) when rendering the pseudocode to an image:
**mermaid-config.json**
```json
{
"theme": "base",
"themeVariables": {
"fontFamily": "Inter, Segoe UI, Arial",
"fontSize": "16px",
"lineColor": "#475569",
"mainBkg": "#ffffff",
"cScale0": "#0f172a",
"cScaleLabel0": "#ffffff",
"cScale1": "#2563eb",
"cScaleLabel1": "#ffffff",
"cScale2": "#059669",
"cScaleLabel2": "#ffffff",
"cScale3": "#d97706",
"cScaleLabel3": "#ffffff",
"cScale4": "#6366f1",
"cScaleLabel4": "#ffffff",
"nodeBorder": "#cbd5e1",
"clusterBkg": "#f8fafc"
}
}
```
## Finally, we need a Python script
We've our Docker image, we've our configuration files. The last thing is writing a Python script that will do "something", generate a Mermaid pseudocode and render that pseudocode as an image.
What can we do? Whatever you want to do! Just use your preferred AI engine and ask it to create the corresponding Python script.
Let's try two things:
### We'll read a Docker compose.yaml and create a mindmap
The script below will read a `compose.yaml` file and generate a visual mindmap. *There is also a ready-made tool for this, see [How to generate a graph from compose.yaml](/blog/docker-compose-viz), but writing the script ourselves gives us total freedom on the output.*
**mindmap.py**
```python
import yaml
import argparse
import subprocess
import os
def get_emoji(name):
name = name.lower()
if any(x in name for x in ['db', 'postgres', 'sql', 'redis', 'cache']):
return "🗄️"
if any(x in name for x in ['nginx', 'proxy', 'gateway', 'api']):
return "🌐"
if 'auth' in name:
return "🔐"
return "📦"
def generate_mindmap(compose_path):
with open(compose_path, 'r') as f:
data = yaml.safe_load(f)
lines = ["mindmap", " root(( 🏗️ INFRASTRUCTURE ))"]
lines.append(" Services")
for name, config in data.get('services', {}).items():
emoji = get_emoji(name)
lines.append(f" ({emoji} {name})")
if 'depends_on' in config:
for dep in config['depends_on']:
lines.append(f" {{{{ 🔗 {dep} }}}}")
lines.append(" Networks")
for net in data.get('networks', {}).keys():
lines.append(f" ) {net} (")
lines.append(" Volumes")
for vol in data.get('volumes', {}).keys():
lines.append(f" [{vol}]")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("input", help="compose.yaml file")
parser.add_argument("-o", "--output", default="infra_map.png")
parser.add_argument("--style", default="/app/mermaid-config.json")
args = parser.parse_args()
mmd_code = generate_mindmap(args.input)
with open("temp.mmd", "w") as f:
f.write(mmd_code)
print("🚀 Generating Pro diagram...")
cmd = [
"mmdc",
"-i", "temp.mmd",
"-o", args.output,
"--width", "1600",
"-p", "/app/puppeteer-config.json",
"--backgroundColor", "transparent"
]
if os.path.exists(args.style):
cmd.extend(["-c", args.style])
subprocess.run(cmd, check=True)
os.remove("temp.mmd")
print(f"✨ Done! Image: {args.output}")
if __name__ == "__main__":
main()
```
Here is the dummy `compose.yaml` I'll use as an example:
**compose.yaml**
```yaml
services:
nginx-proxy:
image: nginx:alpine
ports: ["80:80", "443:443"]
networks: [frontend]
volumes: ["./nginx.conf:/etc/nginx/nginx.conf"]
api-gateway:
build: ./services/gateway
depends_on: [auth-service, user-service]
networks: [frontend, backend]
environment:
- DB_URL=postgres://db:5432/app
auth-service:
image: node:18
networks: [backend]
volumes: ["auth_data:/data"]
user-service:
image: python:3.11
networks: [backend, database]
environment:
- REDIS_URL=redis:6379
db-primary:
image: postgres:15
networks: [database]
volumes: ["pg_data:/var/lib/postgresql/data"]
environment:
- POSTGRES_PASSWORD=secret
redis-cache:
image: redis:alpine
networks: [database]
networks:
frontend:
backend:
database:
driver: bridge
volumes:
pg_data:
auth_data:
```
Now, the best part: please run this command to render the image:
```bash
$ docker run --rm -u $(id -u):$(id -g) -e HOME=/tmp -v .:/app yourself/docker-python-mermaid python3 /app/scripts/mindmap.py /app/samples/compose.yaml -o /app/compose.png
Generating single mermaid chart
🚀 Generating Pro diagram...
✨ Done! Image: /app/compose.png
```
Using a single command line produces the mindmap shown at the top of this article. This image has been generated by the `mindmap.py` Python script once our `compose.yaml` file was processed. We can now change our YAML file, run the `docker run` command again, and the image will be updated automatically.
> **Colors:**
>
> Remember, simply update the `mermaid-config.json` file if you want another color scheme.
## More Examples
### Another example
This script will look at our file structure, identify the files we have, and create a mindmap as well:
**project-dna.py**
```python
import os
import argparse
import subprocess
def get_category(filename):
ext = os.path.splitext(filename)[1].lower()
name = filename.lower()
if name == 'dockerfile' or ext == '.dockerfile': return '🐳 Docker'
if ext in ['.py', '.sh', '.js', '.go']: return '💻 Code'
if ext in ['.yaml', '.yml', '.json', '.conf', '.ini']: return '⚙️ Config'
if ext in ['.md', '.txt', '.pdf']: return '📝 Doc'
if ext in ['.png', '.jpg', '.svg', '.ico']: return '🎨 Assets'
return '📄 Other'
def generate_dna(path):
structure = {}
# Analyze files
for root, dirs, files in os.walk(path):
# Ignore hidden and heavy folders
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for file in files:
cat = get_category(file)
structure.setdefault(cat, set()).add(file)
if not structure:
return "mindmap\n root(( 🧬 PROJECT DNA ))\n (Empty or unreadable folder)"
lines = ["mindmap", " root(( 🧬 PROJECT DNA ))"]
for cat, files in structure.items():
lines.append(f" {cat}")
# Show max 6 files per category to avoid cluttering the image
for i, f in enumerate(sorted(list(files))):
if i >= 6:
lines.append(" (...)")
break
# Clean special characters for Mermaid
clean_name = f.replace('(', '').replace(')', '').replace('[', '').replace(']', '')
lines.append(f" ({clean_name})")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("path", help="Folder to scan")
parser.add_argument("-o", "--output", default="dna.png")
args = parser.parse_args()
# Ensure the path is absolute
target_path = os.path.abspath(args.path)
mmd_code = generate_dna(target_path)
with open("temp.mmd", "w", encoding='utf-8') as f:
f.write(mmd_code)
print(f"🔎 Analyzing {target_path}...")
subprocess.run([
"mmdc", "-i", "temp.mmd", "-o", args.output,
"-p", "/app/puppeteer-config.json",
"-c", "/app/mermaid-config.json",
"--width", "1200"
], check=True)
os.remove("temp.mmd")
print(f"✨ DNA successfully generated in {args.output}")
if __name__ == "__main__":
main()
```
```bash
$ docker run --rm -u $(id -u):$(id -g) -e HOME=/tmp -v .:/app yourself/docker-python-mermaid python3 /app/scripts/project-dna.py /app -o /app/project-dna.png
Generating single mermaid chart
🔎 Analyzing /app...
✨ DNA successfully generated in /app/project-dna.png
```

This mindmap will not just show us our folders but will group files into categories like `Code`, `Configuration`, `Assets`, etc.
### Last example
This time a Pie chart:
**pie.py**
```python
import os
import argparse
import subprocess
def get_category(filename):
ext = os.path.splitext(filename)[1].lower()
name = filename.lower()
if name == 'dockerfile' or ext == '.dockerfile': return '🐳 Docker'
if ext in ['.py', '.sh', '.js', '.go']: return '💻 Code'
if ext in ['.yaml', '.yml', '.json', '.conf', '.ini']: return '⚙️ Config'
if ext in ['.md', '.txt', '.pdf']: return '📝 Doc'
if ext in ['.png', '.jpg', '.svg', '.ico']: return '🎨 Assets'
return '📄 Other'
def generate_pie_chart(path):
counts = {}
# Analyze files
for root, dirs, files in os.walk(path):
# Ignore hidden and heavy folders
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for file in files:
cat = get_category(file)
counts[cat] = counts.get(cat, 0) + 1
if not counts:
return "pie title Empty or unreadable folder\n \"None\" : 1"
lines = ["pie title Project File Type Distribution"]
for cat, count in counts.items():
# Mermaid pie charts use raw numbers and automatically calculate percentages
lines.append(f' "{cat}" : {count}')
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("path", help="Folder to scan")
parser.add_argument("-o", "--output", default="file_types_pie.png")
args = parser.parse_args()
# Ensure the path is absolute
target_path = os.path.abspath(args.path)
mmd_code = generate_pie_chart(target_path)
with open("temp.mmd", "w", encoding='utf-8') as f:
f.write(mmd_code)
print(f"🔎 Analyzing {target_path}...")
subprocess.run([
"mmdc", "-i", "temp.mmd", "-o", args.output,
"-p", "/app/puppeteer-config.json",
"-c", "/app/mermaid-config.json",
"--width", "1200",
"--backgroundColor", "transparent"
], check=True)
os.remove("temp.mmd")
print(f"✨ Pie chart successfully generated in {args.output}")
if __name__ == "__main__":
main()
```
```bash
$ docker run --rm -u $(id -u):$(id -g) -e HOME=/tmp -v .:/app yourself/docker-python-mermaid python3 /app/scripts/pie.py /app -o /app/pie.png
Generating single mermaid chart
🔎 Analyzing /app...
✨ Pie chart successfully generated in /app/pie.png
```

## All needed files
**Project: /tmp/mermaid**
> **Note:**
>
> Now, please run 'docker build -t yourself/docker-python-mermaid .' to
> create the Docker image.
**Dockerfile**
```docker
FROM node:20-alpine
RUN apk add --no-cache \
chromium \
nss \
freetype \
harfbuzz \
ca-certificates \
ttf-freefont \
font-noto-emoji \
python3 \
py3-pip \
py3-yaml
RUN fc-cache -fv
RUN npm install -g @mermaid-js/mermaid-cli
RUN mkdir -p /home/node/.cache/puppeteer && \
chmod -R 777 /home/node
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
ENV HOME=/home/node
WORKDIR /app
```
**puppeteer-config.json**
```json
{
"args": [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-accelerated-2d-canvas",
"--no-first-run",
"--no-zygote",
"--disable-gpu"
]
}
```
**mermaid-config.json**
```json
{
"theme": "base",
"themeVariables": {
"fontFamily": "Inter, Segoe UI, Arial",
"fontSize": "16px",
"lineColor": "#475569",
"mainBkg": "#ffffff",
"cScale0": "#0f172a",
"cScaleLabel0": "#ffffff",
"cScale1": "#2563eb",
"cScaleLabel1": "#ffffff",
"cScale2": "#059669",
"cScaleLabel2": "#ffffff",
"cScale3": "#d97706",
"cScaleLabel3": "#ffffff",
"cScale4": "#6366f1",
"cScaleLabel4": "#ffffff",
"nodeBorder": "#cbd5e1",
"clusterBkg": "#f8fafc"
}
}
```
**scripts/mindmap.py**
```python
import yaml
import argparse
import subprocess
import os
def get_emoji(name):
name = name.lower()
if any(x in name for x in ['db', 'postgres', 'sql', 'redis', 'cache']):
return "🗄️"
if any(x in name for x in ['nginx', 'proxy', 'gateway', 'api']):
return "🌐"
if 'auth' in name:
return "🔐"
return "📦"
def generate_mindmap(compose_path):
with open(compose_path, 'r') as f:
data = yaml.safe_load(f)
lines = ["mindmap", " root(( 🏗️ INFRASTRUCTURE ))"]
lines.append(" Services")
for name, config in data.get('services', {}).items():
emoji = get_emoji(name)
lines.append(f" ({emoji} {name})")
if 'depends_on' in config:
for dep in config['depends_on']:
lines.append(f" {{{{ 🔗 {dep} }}}}")
lines.append(" Networks")
for net in data.get('networks', {}).keys():
lines.append(f" ) {net} (")
lines.append(" Volumes")
for vol in data.get('volumes', {}).keys():
lines.append(f" [{vol}]")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("input", help="compose.yaml file")
parser.add_argument("-o", "--output", default="infra_map.png")
parser.add_argument("--style", default="/app/mermaid-config.json")
args = parser.parse_args()
mmd_code = generate_mindmap(args.input)
with open("temp.mmd", "w") as f:
f.write(mmd_code)
print("🚀 Generating Pro diagram...")
cmd = [
"mmdc",
"-i", "temp.mmd",
"-o", args.output,
"--width", "1600",
"-p", "/app/puppeteer-config.json",
"--backgroundColor", "transparent"
]
if os.path.exists(args.style):
cmd.extend(["-c", args.style])
subprocess.run(cmd, check=True)
os.remove("temp.mmd")
print(f"✨ Done! Image: {args.output}")
if __name__ == "__main__":
main()
```
**scripts/pie.py**
```python
import os
import argparse
import subprocess
def get_category(filename):
ext = os.path.splitext(filename)[1].lower()
name = filename.lower()
if name == 'dockerfile' or ext == '.dockerfile': return '🐳 Docker'
if ext in ['.py', '.sh', '.js', '.go']: return '💻 Code'
if ext in ['.yaml', '.yml', '.json', '.conf', '.ini']: return '⚙️ Config'
if ext in ['.md', '.txt', '.pdf']: return '📝 Doc'
if ext in ['.png', '.jpg', '.svg', '.ico']: return '🎨 Assets'
return '📄 Other'
def generate_pie_chart(path):
counts = {}
# Analyze files
for root, dirs, files in os.walk(path):
# Ignore hidden and heavy folders
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for file in files:
cat = get_category(file)
counts[cat] = counts.get(cat, 0) + 1
if not counts:
return "pie title Empty or unreadable folder\n \"None\" : 1"
lines = ["pie title Project File Type Distribution"]
for cat, count in counts.items():
# Mermaid pie charts use raw numbers and automatically calculate percentages
lines.append(f' "{cat}" : {count}')
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("path", help="Folder to scan")
parser.add_argument("-o", "--output", default="file_types_pie.png")
args = parser.parse_args()
# Ensure the path is absolute
target_path = os.path.abspath(args.path)
mmd_code = generate_pie_chart(target_path)
with open("temp.mmd", "w", encoding='utf-8') as f:
f.write(mmd_code)
print(f"🔎 Analyzing {target_path}...")
subprocess.run([
"mmdc", "-i", "temp.mmd", "-o", args.output,
"-p", "/app/puppeteer-config.json",
"-c", "/app/mermaid-config.json",
"--width", "1200",
"--backgroundColor", "transparent"
], check=True)
os.remove("temp.mmd")
print(f"✨ Pie chart successfully generated in {args.output}")
if __name__ == "__main__":
main()
```
**samples/compose.yaml**
```yaml
services:
nginx-proxy:
image: nginx:alpine
ports: ["80:80", "443:443"]
networks: [frontend]
volumes: ["./nginx.conf:/etc/nginx/nginx.conf"]
api-gateway:
build: ./services/gateway
depends_on: [auth-service, user-service]
networks: [frontend, backend]
environment:
- DB_URL=postgres://db:5432/app
auth-service:
image: node:18
networks: [backend]
volumes: ["auth_data:/data"]
user-service:
image: python:3.11
networks: [backend, database]
environment:
- REDIS_URL=redis:6379
db-primary:
image: postgres:15
networks: [database]
volumes: ["pg_data:/var/lib/postgresql/data"]
environment:
- POSTGRES_PASSWORD=secret
redis-cache:
image: redis:alpine
networks: [database]
networks:
frontend:
backend:
database:
driver: bridge
volumes:
pg_data:
auth_data:
```
**scripts/project-dna.py**
```python
import os
import argparse
import subprocess
def get_category(filename):
ext = os.path.splitext(filename)[1].lower()
name = filename.lower()
if name == 'dockerfile' or ext == '.dockerfile': return '🐳 Docker'
if ext in ['.py', '.sh', '.js', '.go']: return '💻 Code'
if ext in ['.yaml', '.yml', '.json', '.conf', '.ini']: return '⚙️ Config'
if ext in ['.md', '.txt', '.pdf']: return '📝 Doc'
if ext in ['.png', '.jpg', '.svg', '.ico']: return '🎨 Assets'
return '📄 Other'
def generate_dna(path):
structure = {}
# Analyze files
for root, dirs, files in os.walk(path):
# Ignore hidden and heavy folders
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for file in files:
cat = get_category(file)
structure.setdefault(cat, set()).add(file)
if not structure:
return "mindmap\n root(( 🧬 PROJECT DNA ))\n (Empty or unreadable folder)"
lines = ["mindmap", " root(( 🧬 PROJECT DNA ))"]
for cat, files in structure.items():
lines.append(f" {cat}")
# Show max 6 files per category to avoid cluttering the image
for i, f in enumerate(sorted(list(files))):
if i >= 6:
lines.append(" (...)")
break
# Clean special characters for Mermaid
clean_name = f.replace('(', '').replace(')', '').replace('[', '').replace(']', '')
lines.append(f" ({clean_name})")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("path", help="Folder to scan")
parser.add_argument("-o", "--output", default="dna.png")
args = parser.parse_args()
# Ensure the path is absolute
target_path = os.path.abspath(args.path)
mmd_code = generate_dna(target_path)
with open("temp.mmd", "w", encoding='utf-8') as f:
f.write(mmd_code)
print(f"🔎 Analyzing {target_path}...")
subprocess.run([
"mmdc", "-i", "temp.mmd", "-o", args.output,
"-p", "/app/puppeteer-config.json",
"-c", "/app/mermaid-config.json",
"--width", "1200"
], check=True)
os.remove("temp.mmd")
print(f"✨ DNA successfully generated in {args.output}")
if __name__ == "__main__":
main()
```
## Conclusion
By combining the parsing power of Python with the rendering elegance of Mermaid, we've turned static documentation into a dynamic asset.
If you like the *Documentation as Code* approach, have a look at two other articles of mine going in the same direction: [Docker - Diagrams as code](/blog/docker-diagram-as-code) and [Python - Generate flows using pydot](/blog/python-pydot). And when there is no parser to write at all — a sentence describing a process, a config format I have no script for — [ai-diagram](/blog/ollama-ai-diagram) asks a local LLM for the Mermaid instead.
---