Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

n8n Puppeteer Node: How to Self-Host Browser Automation on a PaaS Without It Eating the Host

Sean

Platform Writer

Jun 17, 2026
8 min read

Self-hosting the n8n Puppeteer node requires Puppeteer, a Chromium download, system libraries, a persistent profile directory, and a memory limit. Skip any of those and the node will either fail to launch, eat 4GB of RAM per task, or leave zombie Chromium processes that crash the host. The default npm install puppeteer pulls ~300MB of Chromium, runs it inline in the n8n worker, and never cleans up the browser process. On a typical PaaS deploy with 1GB of RAM, the first Puppeteer task will work. The second will work. The tenth will OOM the worker. The hundredth will trigger the deploy platform’s auto-restart loop, and the next deploy will fail because the disk is full of Chromium caches.

This post is the version that ships. The first half is the dependency setup (Chromium, system libraries, the n8n-nodes-puppeteer community node). The second half is the runtime setup (persistent profile, memory limit, process cleanup, the deploy platform contract that turns Puppeteer from a liability into a feature).

The interesting thing about Puppeteer in n8n is that the failure mode is always the same: the host runs out of a resource (memory, disk, CPU, file handles) because Puppeteer is greedy and the integration does not limit it. The fix is the boring version of the integration: explicit resource limits, explicit cleanup, explicit isolation.

n8n Puppeteer Node: How to Self-Host Browser Automation on a PaaS Without It Eating the Host

Table of contents

The direct answer

For most self-hosted n8n setups, the right pattern is:

  1. Run browserless/chromium as a sidecar container on the same network.
  2. Use puppeteer-core (not puppeteer) in the n8n container.
  3. Point n8n-nodes-puppeteer at the Browserless service over the internal network.
  4. Set a memory limit on the n8n worker (512MB is a sane starting point).
  5. Use a persistent volume for the browser profile so it survives restarts.

The result: Puppeteer runs in a managed Chromium, n8n talks to it over HTTP, the memory is bounded, the profile is persistent, and the deploy is reproducible. The default install pulls its own Chromium and runs everything in the n8n process; the alternative uses a sidecar and respects the platform’s memory limits.

The rest of the post is the why.

The three install paths

There are three common ways to add Puppeteer to a self-hosted n8n:

Path 1: vanilla npm install puppeteer. The default. Installs Puppeteer, downloads a pinned Chromium (~300MB), runs Chromium inline in the n8n process per task. Works for a single task, fails for a queue.

Path 2: puppeteer-core plus a managed Chromium. Puppeteer is just the protocol client; the Chromium binary is provided externally. The team picks the Chromium (system Chromium, Browserless, a different headless browser), and puppeteer-core connects to it.

Path 3: Browserless as a sidecar. A separate container running Browserless (or Chrome in headless-shell mode). n8n talks to it over HTTP. The Chromium is isolated from the n8n process, the memory is bounded, and the sidecar can be scaled independently.

The three paths are not equal. Path 1 is what every tutorial shows. Path 2 is what most teams should use. Path 3 is what the teams that run Puppeteer in production use. The progression is “less memory pressure, more isolation.”

Path 1: vanilla npm install puppeteer (the trap)

The default Puppeteer install does four things:

  • Adds puppeteer to node_modules.
  • Downloads a pinned Chromium binary (~300MB) to ~/.cache/puppeteer/.
  • Installs system libraries required by Chromium (libnss3, libatk, libcups, libdrm, libxkbcommon, libxcomposite, libxdamage, libxrandr, libgbm, libpango, libcairo, libasound2, fonts).
  • Provides a puppeteer.launch() API that starts a Chromium subprocess.

On a Linux server with all the system libraries, this works. The first puppeteer.launch() starts Chromium, runs the task, and exits. The next task starts a new Chromium. The system handles a handful of these before running out of memory or file handles.

The failure modes:

  • Memory. Chromium uses 200-500MB of RAM per instance. The n8n process is the parent, the Chromium subprocess is the child. A 1GB n8n worker can run one Chromium task before the OOM killer kicks in. A 4GB worker can run four or five. The cost is linear in the number of concurrent tasks.
  • Disk. The Chromium download is 300MB. The cache directory fills with downloaded versions as the team upgrades. A Docker image with Puppeteer is at minimum 500MB larger than the same image without. The base image plus Chromium plus node_modules is the image size floor.
  • CPU. Chromium uses 1-2 cores per task. A multi-core n8n worker handles a few tasks, then the CPU is saturated. The tasks queue up, the latency grows, and the deploy’s response time degrades.
  • Zombie processes. A crashed task leaves the Chromium subprocess running. The next task starts another Chromium. The host runs out of PIDs or memory before the next task completes. The deploy platform’s auto-restart triggers, the restart fails, the deploy is broken.

The default path is the trap. The fix is to get the Chromium out of the n8n process.

Path 2: puppeteer-core plus a managed Chromium (the better answer)

puppeteer-core is the protocol client without the bundled Chromium. The team provides the Chromium themselves:

npm install puppeteer-core

Then the team connects to a managed Chromium — a system Chromium, a Browserless container, or a Chrome in headless-shell mode:

const puppeteer = require('puppeteer-core');

const browser = await puppeteer.connect({
  browserWSEndpoint: 'ws://browserless:3000',
});

The benefits:

  • Memory isolation. The Chromium runs in a separate process, with a separate memory budget. The n8n process is bounded; the Chromium process is bounded; the two do not share a memory limit.
  • Version control. The team pins the Chromium version separately from the Puppeteer version. The Chromium can be upgraded without changing the n8n code, and vice versa.
  • Easier debugging. The Chromium can be debugged in isolation, with its own logs, its own metrics, its own lifecycle.
  • No 300MB download. The puppeteer-core package is small. The Chromium is a separate container, a separate deploy, a separate concern.

The trade: the team now has two services to operate (n8n and the Chromium). The operational complexity is higher. For a team that runs n8n in production, the complexity is the right trade.

Path 3: Browserless as a sidecar (the right answer for most)

Browserless is a managed Chromium-as-a-service. It runs in a Docker container, exposes a WebSocket endpoint, and handles the Chromium lifecycle (spawn, restart, memory limits, queue) for the team.

The setup:

# docker-compose.yml
services:
  n8n:
    image: n8nio/n8n
    environment:
      - PUPPETEER_WS_ENDPOINT=ws://browserless:3000
    depends_on:
      - browserless

  browserless:
    image: browserless/chromium
    environment:
      - MAX_CONCURRENT_SESSIONS=5
      - QUEUE_LENGTH=10
      - MEMORY_LIMIT=512MB
    ports:
      - "3000:3000"
    shm_size: 2gb

The n8n container runs the workflow, the Browserless container runs the Chromium. The two communicate over the internal Docker network. The memory is bounded (the Browserless container is 512MB), the queue is bounded (10 sessions max), and the disk is shared via the shm_size.

The benefits over Path 2:

  • Operational maturity. Browserless handles the Chromium lifecycle. The team does not write a custom Chromium supervisor.
  • Session limits. Browserless enforces a max concurrent sessions, so the team cannot accidentally OOM the host.
  • Queue. When the session limit is hit, Browserless queues the next request and returns a 429 with a Retry-After. The n8n workflow can handle the 429 and retry.
  • Observability. Browserless exposes Prometheus metrics, structured logs, and a health endpoint. The team can see what is happening.

The trade: Browserless is a third-party image. The team trusts it to run the Chromium correctly. For most teams, the trust is well-placed — Browserless is well-maintained, well-tested, and the source is open.

For a self-hosted n8n, the Browserless sidecar is the right answer.

The n8n community node

The n8n-nodes-puppeteer community node wraps Puppeteer for n8n workflows. The install is the standard n8n community-node flow:

# Inside the n8n container
npm install n8n-nodes-puppeteer

Or, in a custom Docker image:

FROM n8nio/n8n
RUN npm install -g n8n-nodes-puppeteer

The community node accepts a WebSocket endpoint for the Chromium. The right configuration is the Browserless URL, not the default puppeteer.launch():

{
  "node": "n8n-nodes-puppeteer.puppeteer",
  "parameters": {
    "operation": "screenshot",
    "url": "https://example.com",
    "browserlessEndpoint": "ws://browserless:3000"
  }
}

The community node supports a browserlessEndpoint parameter. Set it to the Browserless service URL. The node will connect over WebSocket and run the task on the managed Chromium.

The default value (puppeteer.launch()) is the trap. Always set the browserlessEndpoint to point at the sidecar.

The runtime memory limit

The n8n process should have a memory limit. The Browserless process should have a memory limit. The two limits are independent, and the two limits together are the total memory the automation stack can use.

A reasonable starting point:

  • n8n worker: 1-2GB. The worker handles the workflow logic, the queue management, and the WebSocket client. Puppeteer is not running in the worker; the worker is just coordinating.
  • Browserless: 1-2GB. The Browserless handles the Chromium, the session queue, and the rendering. The Chromium is bounded; the queue is bounded; the disk is bounded.

For higher concurrency, scale the Browserless sidecar horizontally. The n8n worker stays the same; the Browserless sidecar scales to handle the load.

The trap: a single memory limit on the host. The n8n worker and the Browserless sidecar share the host’s memory. If the host has 4GB, the two services together should not exceed 4GB. The right pattern is to set explicit memory limits on each container, not on the host.

The persistent profile that does not leak

A Puppeteer profile is a directory of cookies, local storage, IndexedDB, and cached resources. The profile is per-task, per-user, or per-website. The right answer depends on the use case.

For one-off tasks (screenshot a URL, scrape a page, render a PDF), the profile is throwaway. A fresh profile per task is the right answer. The task creates the profile, runs the automation, deletes the profile. No persistence, no leak.

For authenticated tasks (log in, scrape a dashboard, automate a workflow), the profile must persist. The team needs a persistent volume for the profile directory, and the profile must be cleaned periodically to avoid the disk filling up.

The trap: a persistent profile that is never cleaned. The cookies expire, the local storage fills, the IndexedDB grows. The profile becomes hundreds of megabytes. The disk fills. The deploy crashes.

The fix: a profile cleanup task. Once a day, delete profiles older than 7 days. Once an hour, delete the cache directories. The cleanup is a cron job in n8n, and the cron is the team’s protection against the leak.

The deploy platform contract

The deploy platform contract for n8n + Puppeteer + Browserless:

  • The n8n container has a memory limit. The platform enforces it.
  • The Browserless container has a memory limit. The platform enforces it.
  • The n8n and Browserless containers are on the same private network. The platform routes between them.
  • The persistent profile volume is on a real volume, not the container’s writable layer. The profile survives restarts.
  • The platform surfaces the Chromium version, the Browserless version, and the n8n version. The team can see what is running.

A platform with these features turns the n8n + Puppeteer stack from a Friday-afternoon fire into a one-click deploy. A platform without them is a platform where the team is debugging Chromium memory leaks for the rest of the quarter.

The RunxBuild platform handles the container, the memory limits, the private networking, and the persistent volumes. The team focuses on the workflow.

For a sanity check on the deploy cost, the hosting cost calculator gives a real number to compare against. The Browserless sidecar is not free; the cost is the price of isolation.

The opinion this post is built on

Puppeteer in n8n is a memory and disk trap, and the default install is the trap. The team that follows the tutorial ends up with a 1GB n8n worker that runs one Chromium task before OOMing. The team that scales n8n ends up with a 4GB worker that runs four tasks before the disk fills. The team that runs Puppeteer in production has a Browserless sidecar and a memory limit, and the trap is gone.

The pattern is the same as every other resource-greedy tool: isolate it, bound it, observe it. The default install runs Puppeteer in the n8n process. The production install runs Puppeteer in a sidecar. The default install has no memory limit. The production install has a memory limit and a session limit. The default install has no observability. The production install has Prometheus metrics and structured logs.

The platform is the multiplier. A platform with container memory limits, private networking, persistent volumes, and observability turns the n8n + Puppeteer stack into a deployable service. A platform without those features forces the team to operate the runtime, and the team will get it wrong in a different way for every service.

The n8n + Puppeteer stack is a powerful automation platform. The default install is a liability. The production install is a feature. The difference is the sidecar, the memory limit, and the platform.

FAQ

Does n8n include Puppeteer by default?

No. Puppeteer is a community node, installed via npm install n8n-nodes-puppeteer. The community node accepts a browserlessEndpoint parameter, which the team should set to point at a managed Chromium. The default value (Puppeteer’s own Chromium) is the trap.

How much memory does Puppeteer use?

200-500MB per Chromium instance, depending on the page and the task. A single n8n worker can run a handful of concurrent Chromium tasks before the OOM killer kicks in. The right pattern is to run Chromium in a sidecar (Browserless) with its own memory limit, not in the n8n worker.

What is Browserless?

Browserless is a managed Chromium-as-a-service that runs in a Docker container. It exposes a WebSocket endpoint, enforces session and queue limits, and handles the Chromium lifecycle. The n8n community Puppeteer node can connect to it via the browserlessEndpoint parameter. Browserless is the right answer for most self-hosted n8n + Puppeteer setups.

Why does my n8n + Puppeteer stack crash every few hours?

Almost always because Chromium is leaking memory. The fix is the Browserless sidecar with a memory limit. The next most common cause is zombie Chromium processes from crashed tasks. The fix is the Browserless lifecycle, which kills the Chromium when the task ends.

Can I use a different headless browser?

Yes. puppeteer-core accepts any Chromium-compatible WebSocket endpoint. The team can use Chrome in headless-shell mode, Playwright with the Chromium driver, or a custom Chromium build. Browserless is the easiest option; the alternatives are for teams that need a specific browser version or feature.

Should I commit the n8n + Puppeteer Docker image?

The image is the build artifact. The team should rebuild it on dependency changes (a Puppeteer version bump, a Browserless version bump, an n8n version bump). The platform should rebuild and redeploy automatically. The image is the contract; the lockfile is the dependency contract; the platform is the deploy contract. Each layer does one job.

#n8n puppeteer node#n8n puppeteer#self-host puppeteer#browserless puppeteer#n8n browser automation#puppeteer memory