Render Chart.js Charts as PNG Images Server-Side (Without Fighting node-canvas)

Server-side Chart.js to PNG looks simple until you actually try it. The canonical path runs through node-canvas, which needs Cairo, Pango, libpng, jpeg,…

Server-side Chart.js to PNG looks simple until you actually try it. The canonical path runs through node-canvas, which needs Cairo, Pango, libpng, jpeg, giflib, and librsvg — native dependencies that break on macOS and exceed serverless function size limits [1][2]. Chart.js issue #8831 proposed node-canvas as the fix, but that path still leaves you fighting install errors, blurry output, and plugins that don't render [3][4][5][6]. The easier route: write the chart as HTML, send it to a browser engine via API, and get back a pixel-perfect PNG buffer.

Why node-canvas and chartjs-node-canvas keep breaking

Chart.js is built on the HTML5 canvas element, so using it in Node.js requires mimicking that element [4]. The standard wrapper, chartjs-node-canvas, inherits node-canvas's native dependency stack [7]. Users hit "Error: Cannot find module 'canvas'" repeatedly, and the package's own readme states it is limited by the upstream dependency canvas [8]. Installing those native libraries on macOS requires Homebrew packages like cairo and pango [2].

chartjs-node-canvas also disables Chart.js animation and responsive resize because Node lacks the browser APIs that drive them [9]. Gradients render differently than in a real browser, and some plugins don't work at all [6]. A Stack Overflow user reported that images generated by the plugin were too blurred [4], and a Reddit user hit "'candlestick' is not a registered controller" when using chartjs-node-canvas with financial chart types [5].

On Vercel, a function using chartjs-node-canvas exceeded the 50MB limit at 50.55MB, with the canvas dependency alone taking 42.7MB [1]. AWS Lambda requires workarounds like building on an Amazon Linux 2 EC2 instance and checking compiled node_modules into git, which defeats the purpose of serverless [10][11].

Set up the MarkupGo Node client and a chart template

Install the client and initialize it with your API key [12]:

npm install markupgo-node
import MarkupGo from "markupgo-node";

const markupgo = new MarkupGo({
  API_KEY: process.env.MARKUPGO_API_KEY,
});

The template needs an HTML canvas element, Chart.js loaded from a CDN in the libraries field, and your chart configuration passed via context [13]. Here's the structure:

const templateData = {
  id: "YOUR_TEMPLATE_ID", // create this in the MarkupGo dashboard
  context: {
    labels: ["Jan", "Feb", "Mar", "Apr", "May"],
    values: [12, 19, 8, 15, 22],
    title: "Monthly Active Users"
  },
  autoHeight: true
};
<div style="width: 800px; padding: 40px; background: white;">
  <canvas id="chart"></canvas>
</div>
<script>
  const ctx = document.getElementById('chart').getContext('2d');
  new Chart(ctx, {
    type: 'bar',
    data: {
      labels: {{ labels }},
      datasets: [{
        label: {{ title }},
        data: {{ values }},
        backgroundColor: 'rgba(99, 102, 241, 0.6)'
      }]
    },
    options: {
      responsive: false,
      animation: false,
      plugins: {
        legend: { display: true }
      }
    }
  });
</script>

The libraries field in your template includes https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js [13]. Set autoHeight: true so the output size fits your content rather than clipping to a fixed height [13].

Render the chart to PNG and extend to PDF reports

Call image.fromTemplate with your template ID, context, and format set to png [12].

import * as fs from "fs";

// Get back a buffer and write to file
const buffer = await markupgo.image.fromTemplate(templateData, {
  properties: {
    format: "png",
    width: 880,
    height: 600
  }
}).buffer();

fs.writeFileSync("chart.png", Buffer.from(buffer));

Supported output formats are PNG, JPEG, and WebP [12]. The same template drives PDF generation too — swap image.fromTemplate for pdf.fromTemplate to embed the chart in a full report [12]:

const pdfBuffer = await markupgo.pdf.fromTemplate(templateData, {
  properties: {
    printBackground: true,
    landscape: true
  }
}).buffer();

fs.writeFileSync("report.pdf", Buffer.from(pdfBuffer));

For no-code use cases, enable the Magic Template URL on your template and generate images from query parameters without writing any code [14]. The URL structure is https://render.markupgo.com/template/{templateId}.png?labels=Jan,Feb,Mar&values=12,19,8&title=MAU. Responses cache for 15 days, so reusing the same URL doesn't consume additional credits [14].

Pricing and getting started

MarkupGo runs a perpetual free trial of 100 credits per month with no credit card required [15]. Paid plans start at Lite for $29 per month with 2,000 credits and 5 templates, Plus at $49 per month with 10,000 credits and 30 templates, and Pro at $99 per month with 20,000 credits and unlimited templates [15].

Sign up for the free tier, create a template with Chart.js in the libraries field, and call image.fromTemplate from your Node service. You'll skip the native dependency headaches entirely and get browser-accurate renders every time.

Sources

  1. Chart Recommendation (reddit.com)
  2. stackoverflow.com
  3. github.com
  4. Creating chart.js chart directly to PNG in Node js? (stackoverflow.com)
  5. nodejs serverside chartjs canvas to png (reddit.com)
  6. Chart.js Server-Side Rendering as Images (Python Guide) (dev.to)
  7. Render a Chart to an Image on the Server (Node + Python, 2026) (dev.to)
  8. Error: Cannot find module 'canvas' (stackoverflow.com)
  9. npmjs.com
  10. How to create serverless images using AWS lambda and ChartJS (medium.com)
  11. Deploy chartjs-node-canvas to AWS lambda using docker container (awstip.com)
  12. markupgo.com
  13. markupgo.com
  14. markupgo.com
  15. markupgo.com