By·

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.

Rendered Mermaid Server-Side Rendering: Generate Diagrams Without a Browser
Rendered example. Copy the first code block below to edit it.

# 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-cli

Basic Usage

mmdc -i diagram.mmd -o diagram.png

Common 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.json

Docker 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.png

The 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/*.png

Approach 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 puppeteer

Render 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 directly

Caveats

  • 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?

NeedBest Approach
CLI / CI / one-off scriptsmermaid-cli (mmdc)
Docker and GitHub Actionsmermaid-cli Docker image
API endpoint with heavy trafficPuppeteer with browser pool
Modern projects, Docker-friendlyPlaywright
Lightweight SVG, can skip perfect fontsMermaid 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.