PDFPipe

CI runners / GitHub Actions

Puppeteer on GitHub Actions: an environment nothing else resembles

The workflow renders documents perfectly. The same code in production fails on missing libraries, missing fonts, or memory.

What is actually wrong

A hosted runner has a browser already installed, several gigabytes of memory, root access and a package manager. It is the most permissive environment your code will ever run in, and it is not the one you are deploying to. A workflow that renders successfully proves the code is correct and proves nothing at all about whether it can be deployed.

What GitHub Actions gives you to work with

an ephemeral runner with a lot of memory, a browser already installed, and a job that ends when the workflow does.

  • Packaging: no packaging step at all. Anything can be installed at the start of the job, which is exactly why this environment misleads people about what production will do.
  • Filesystem: fully writable, and discarded when the job ends.
  • Processes: full control, with a maximum job duration measured in hours. Nothing is shared between runs, so every job pays every setup cost again.

The fix, if you are keeping Puppeteer

If the workflow is the deliverable, meaning documents are genuinely generated in CI, install the browser explicitly rather than relying on whatever the runner image happens to include, so a runner image update does not change your output. If the workflow is a test of production behaviour, run it in the same container the production service uses.

yaml
jobs:
  render:
    # The same image production runs, so the test means something.
    container:
      image: your-registry/renderer:1.4.2
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: node scripts/render.js

What still hurts after that

Running in your own image makes the test meaningful and makes the workflow as slow as the image is large. The deeper issue stands: a hosted runner is a bad model of production, so a green pipeline is weak evidence that a browser-based renderer will deploy. Whatever the constraint turns out to be, it will be found somewhere other than here.

What Puppeteer is genuinely good at

it renders exactly what a browser renders, which means modern CSS, web fonts and client-side JavaScript all behave the way they do in a tab. That is worth keeping in view: the problem on this page is where it runs, not what it produces. Puppeteer is a Node library that drives a real Chromium, and downloads that Chromium into node_modules when it is installed. It needs the browser binary, a long list of shared libraries the browser links against, fonts, and enough memory for a browser process that is not accounted for by your runtime's heap settings.

The other shape of the answer

Every fix above keeps the renderer inside your deployment, which means the constraint stays yours: the package size, the shared libraries, the fonts, the memory, the patching. The alternative is an HTTP call from GitHub Actions to something that already has a browser in it, which turns all of the above into a request and a response. That is what this API is, and it is the reason these pages exist. Whether that trade is right depends on whether rendering is a thing you want to operate.

Frequently asked

Can Puppeteer run on GitHub Actions at all?

If the workflow is the deliverable, meaning documents are genuinely generated in CI, install the browser explicitly rather than relying on whatever the runner image happens to include, so a runner image update does not change your output. If the workflow is a test of production behaviour, run it in the same container the production service uses. Whether that is worth doing is a separate question from whether it is possible, and the section above on what still hurts is the honest answer to it.

Why does it work locally?

Because a development machine has root, a package manager, several gigabytes of memory, a full font set and no packaging limit. Every one of those is a thing GitHub Actions constrains. A renderer that works locally and fails on deploy has not regressed: it has met the first environment that does not give it everything.

Related failures

The same platform with a different renderer, and the same renderer on other platforms.

If the render moves off this platform, none of the above is your problem to keep solving. 100 free documents a month, and a playground that needs no signup.