Mermaid Server-Side Rendering: Generate Diagrams Without a Browser
Learn how to render Mermaid diagrams server-side using mermaid-cli, Puppeteer, Playwright, and Node.js for automated documentation, CI/CD pipelines, and API-driven diagram generation.
# Mermaid Server-Side Rendering: Generate Diagrams Without a Browser
Mermaid is great in the browser. But when you need diagrams in PDFs, automated docs, CI pipelines, or an API, you cannot rely on a user's browser being open. You need server-side rendering.
This guide covers every production-ready approach to rendering Mermaid diagrams on a server: mermaid-cli, Puppeteer, Playwright, and the Node.js Mermaid API.
Why Render Mermaid Server-Side?
Client-side rendering works well for interactive use. Server-side rendering solves different problems:
- Automated documentation — regenerate all diagrams when docs rebuild
- CI/CD pipelines — verify diagrams render correctly in pull requests
- PDF generation — embed diagrams in reports without screenshotting
- Email reports — include diagrams in transactional emails
- API endpoints — expose a "render this Mermaid code" endpoint
- Bulk export — convert hundreds of diagrams to PNG/SVG unattended
- Consistent output — same rendering every time, not per-browser quirks
Approach 1: mermaid-cli (mmdc) — The Official Tool
The official mermaid-cli is the simplest way to render diagrams server-side. It bundles Mermaid with a headless Chromium instance.
Installation
npm install -g @mermaid-js/mermaid-cliBasic Usage
mmdc -i diagram.mmd -o diagram.pngCommon Options
# Output as SVG
mmdc -i input.mmd -o output.svg
# Output as PDF
mmdc -i input.mmd -o output.pdf -f pdf
# Custom theme and background
mmdc -i input.mmd -o output.png -t dark --backgroundColor transparent
# Set resolution for PNG
mmdc -i input.mmd -o output.png --scale 2 --width 1200 --height 800
# Puppeteer config for custom Chromium path
mmdc -i input.mmd -o output.png -p puppeteer-config.jsonDocker Usage
For CI environments, use the official Docker image:
docker run --rm -v $(pwd):/data minlag/mermaid-cli \
-i /data/diagram.mmd -o /data/output.pngThe Docker image includes Chromium and all dependencies. It is the safest way to run mermaid-cli in GitHub Actions, GitLab CI, or Jenkins.
GitHub Actions Example
name: Verify Diagrams
on: [push, pull_request]
jobs:
diagrams:
runs-on: ubuntu-latest
container: minlag/mermaid-cli:latest
steps:
- uses: actions/checkout@v4
- run: |
for f in docs/diagrams/*.mmd; do
mmdc -i "$f" -o "${f%.mmd}.png"
done
- uses: actions/upload-artifact@v4
with:
name: diagrams
path: docs/diagrams/*.pngApproach 2: Mermaid + Puppeteer — Full Control
If mermaid-cli is too limited, you can drive Puppeteer directly. This gives you full control over the rendering pipeline.
Setup
npm install mermaid puppeteerRender to SVG
import puppeteer from "puppeteer";
const diagramCode = `
graph TD
A[Client] --> B[Load Balancer]
B --> C[API Server]
C --> D[Database]
C --> E[Cache]
`;
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
const html = `
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<script>mermaid.initialize({ startOnLoad: true, theme: 'default' });</script>
</head>
<body>
<div class="mermaid">${diagramCode}</div>
</body>
</html>
`;
await page.setContent(html);
await page.waitForSelector("svg");
const svgContent = await page.$eval("svg", (el) => el.outerHTML);
console.log(svgContent);
await browser.close();Render to PNG with Custom Dimensions
const element = await page.$("svg");
const boundingBox = await element.boundingBox();
await page.setViewport({
width: Math.ceil(boundingBox.width) + 80,
height: Math.ceil(boundingBox.height) + 80,
deviceScaleFactor: 2,
});
await element.screenshot({ path: "diagram.png" });Production Tips for Puppeteer
- Reuse browser instances — launching Chrome per request is expensive. Keep a browser pool.
- Set a render timeout — complex diagrams can hang. 30 seconds is reasonable.
- Use --no-sandbox in Docker — add
args: ["--no-sandbox"]to launch options. - Handle errors gracefully — Mermaid syntax errors should return a 400, not crash your server.
Approach 3: Mermaid Node.js API — No Browser Required (Experimental)
Mermaid offers a programmatic API for rendering diagrams to SVG strings without a browser. This is newer and less tested but avoids Chrome entirely.
import mermaid from "mermaid";
mermaid.initialize({ startOnLoad: false });
const { svg } = await mermaid.render("diagram-1", `
graph LR
A[Start] --> B{Is it working?}
B -->|Yes| C[Deploy]
B -->|No| D[Debug]
D --> B
`);
console.log(svg); // SVG string directlyCaveats
- Font rendering — the programmatic API may miss fonts that a browser would render. Text alignment can differ from browser output.
- CSS in SVG — some theme styles rely on CSS that needs a DOM. The API may produce SVG without full styling.
- Not all features work — interactive features (click events, tooltips) depend on a browser runtime.
For production, mermaid-cli or Puppeteer is generally more reliable. The programmatic API is useful when you need fast SVG with minimal overhead and can accept minor visual differences.
Approach 4: Playwright — Modern Alternative to Puppeteer
Playwright has better Docker support and auto-downloads browsers. The code is similar to Puppeteer.
import { chromium } from "playwright";
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.setContent(`
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<script>mermaid.initialize({ startOnLoad: true });</script>
</head>
<body><div class="mermaid">${diagramCode}</div></body>
</html>
`);
await page.waitForSelector("svg");
const element = page.locator("svg");
await element.screenshot({ path: "diagram.png", scale: "css" });
await browser.close();Playwright's scale: "css" option produces sharp screenshots without manual deviceScaleFactor math.
Building a Diagram API Endpoint
Combining these approaches, here is a minimal Express endpoint that renders Mermaid code to PNG:
import express from "express";
import puppeteer from "puppeteer";
const app = express();
app.use(express.json());
let browser;
async function getBrowser() {
if (!browser || !browser.isConnected()) {
browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
}
return browser;
}
app.post("/api/render", async (req, res) => {
const { code, theme = "default", format = "png" } = req.body;
if (!code || code.length > 50000) {
return res.status(400).json({ error: "Invalid diagram code" });
}
try {
const browser = await getBrowser();
const page = await browser.newPage();
const html = `
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<script>
mermaid.initialize({ startOnLoad: true, theme: "${theme}" });
</script>
</head>
<body style="margin:0;padding:16px;background:white;">
<div class="mermaid">${code}</div>
</body>
</html>
`;
await page.setContent(html, { waitUntil: "networkidle0" });
const el = await page.$("svg");
if (!el) {
await page.close();
return res.status(422).json({ error: "Diagram failed to render" });
}
if (format === "svg") {
const svg = await page.$eval("svg", (e) => e.outerHTML);
await page.close();
return res.set("Content-Type", "image/svg+xml").send(svg);
}
const bbox = await el.boundingBox();
await page.setViewport({
width: Math.ceil(bbox.width) + 40,
height: Math.ceil(bbox.height) + 40,
deviceScaleFactor: 2,
});
const screenshot = await el.screenshot({ type: "png" });
await page.close();
res.set("Content-Type", "image/png").send(screenshot);
} catch (err) {
res.status(500).json({ error: "Rendering failed", detail: err.message });
}
});
app.listen(3000);Performance Considerations
Cold start: Launching Chrome takes 1-3 seconds. Keep a warm browser pool.
Memory: Each Chrome instance uses 100-300 MB. In Docker, set --max-old-space-size=512 on Node.js.
Concurrency: One browser can handle ~5-10 concurrent renders with separate pages. Beyond that, pool multiple browsers.
Timeouts: Complex diagrams with many nodes can take 5-15 seconds. Set generous timeouts and consider queuing.
Caching: Cache rendered diagrams by hash of the Mermaid code + theme + format. Most diagrams in docs or CI change rarely.
Which Approach Should You Use?
| Need | Best Approach |
|---|---|
| CLI / CI / one-off scripts | mermaid-cli (mmdc) |
| Docker and GitHub Actions | mermaid-cli Docker image |
| API endpoint with heavy traffic | Puppeteer with browser pool |
| Modern projects, Docker-friendly | Playwright |
| Lightweight SVG, can skip perfect fonts | Mermaid Node.js API |
| Interactive preview (user-facing) | Keep it client-side |
Final Thoughts
Server-side Mermaid rendering is mature enough for production. mermaid-cli covers 80% of use cases, and Puppeteer or Playwright handle the rest.
The key decisions are: Docker or native? Browser pool or single instance? SVG or PNG? Caching or real-time?
Start with mermaid-cli. Add Puppeteer when you need custom behavior. And always set a timeout — a hanging diagram render should not take down your server.
Try it now: paste any Mermaid diagram into MermaidEditor.lol, then use the export options or copy the code for server-side rendering.