Skip to content
This page View Markdown Open in ChatGPT Open in Claude

Browsers in custom APIs

A custom API can drive a headless Chromium with Playwright or Puppeteer : turn HTML into a PDF, take a screenshot, or read a page. A browser needs system libraries next to its npm package, so it runs in one of two places.

Sandbox Native process
Where the code runs In sandboxes of a group of its own, in Docker On the machine of the server (runOnNativeProcess: true)
Where the browser is installed In the Dockerfile of the group : built once, on every server On each server, next to API Maker
Isolation Your code and the browser stay apart from API Maker None : the browser shares the server with API Maker
Set up by An admin, in the settings of the custom API The root user of each server

A browser needs memory : plan 2 GB of RAM and 40 GB of disk for the server, and avoid many browsers at the same time.

Playwright in the sandbox

The sandboxes of an account have no browser : nothing is installed that the account did not add. Give the APIs which drive one a separate sandbox : a group with a Dockerfile which installs Chromium, and the package playwright in the same version. The other code of the account keeps its small image.

Basic info : a group of sandboxes with Chromium
import * as T from 'types';

let customApi: T.ICustomApiSettingsTypes = {
    name: 'Invoice PDF',
    path: '/invoice-pdf',
    requestMethod: T.ERequestMethod.POST,
    apiAccessType: T.EAPIAccessType.TOKEN_ACCESS,
    customApiTimeoutInSeconds: 60,
    separateSandboxSettings: {
        enableSeparateSandboxForThis: 'browsers',   // every API which names this group shares its sandboxes
        sandboxCount: 1,
        packages: {
            allowAllPackagesOfAdmin: false,
            sandboxPackages: [{ name: 'playwright', version: '1.63.0' }],
        },
        dockerFileOfSandbox: `FROM {{AM_BASE_IMAGE}}

# Chromium and the system libraries it needs, in a folder every user of the image reads : built once, above the packages.
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN npx -y [email protected] install --with-deps chromium

{{AM_PACKAGES}}

{{AM_RUNNER}}`,
    },
    errorList: [],
};
module.exports = customApi;
Code : HTML to PDF, sent as a download
import * as T from 'types';
const path = require('path');
const { chromium } = require('playwright');

async function main(g: T.IAMGlobal) {
    const file = `invoice-${g.req.body.number}.pdf`;
    const browser = await chromium.launch({ args: ['--no-sandbox'] });
    try {
        const page = await browser.newPage();
        await page.setContent(`<h1>Invoice ${g.req.body.number}</h1><p>Total : ${g.req.body.total} EUR</p>`);
        await page.pdf({ path: path.join(__dirname, 'uploads', file), format: 'A4', printBackground: true });
    } finally {
        await browser.close();
    }
    // A file of the uploads folder goes out as the answer, and is removed after.
    return { __am__downloadFilePath: file, __am__downloadFolderFileName: file };
}
module.exports = main;
  • Saving the API builds the image of the group on every server, in the background : about a minute the first time, then never again while the Dockerfile and the packages stay the same. A call which comes before waits for it.
  • Keep the version of the package and of playwright install the same : the package looks for the browsers of its own version.
  • --no-sandbox : the container is the sandbox of Chromium.
  • More APIs with a browser name the same group and share its image and sandboxes.

Playwright on the native process

  1. Sign in to the server as root (whoami prints root ; sudo -s otherwise) and install Chromium with the libraries it needs :

    npx playwright install --with-deps chromium
    
  2. Add the package in the folder API Maker runs in (its package.json and .env are there) : the runners of the machine find their packages there.

    cd /path/of/api-maker
    npm i playwright
    
  3. Create a custom API and set runOnNativeProcess: true in its configuration (the Basic Info section of the custom API).

  4. Use it in the code. This one writes a PDF from HTML into the uploads folder :

Custom API : HTML to PDF with Playwright
import * as T from 'types';
import { join } from 'path';
const playwright = require('playwright');

async function main(g: T.IAMGlobal) {
    const browser = await playwright.chromium.launch();
    const page = await browser.newPage();
    await page.setContent('<p style="font: 18px Arial">Hello from Playwright</p>', { waitUntil: 'load' });
    await page.pdf({
        path: join(__dirname, 'uploads', 'playwright.pdf'),
        format: 'A4',
        printBackground: true,
        margin: { top: '20mm', right: '20mm', bottom: '20mm', left: '20mm' },
    });
    await browser.close();

    // uploads/playwright.pdf is on the server : upload it to a storage provider or send it as a download.
    return 'pdf created on the server';
}

module.exports = main;

Every server

What is installed by hand lives on one server : repeat both steps on every server of the cluster, and on each new one. The sandbox has no such step : its image is built on every server from the settings.

Puppeteer on the native process

  1. Sign in to the server as root and install the libraries of Chromium (Ubuntu ; for other systems see the troubleshooting page of Puppeteer) :

    apt update && apt install -y ca-certificates fonts-liberation libatk1.0-0t64 libatk-bridge2.0-0t64 \
      libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 \
      libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 \
      libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 \
      libxss1 libxtst6 libasound2t64 lsb-release wget xdg-utils
    
  2. Add the package in the folder API Maker runs in (its package.json and .env are there) : the runners of the machine find their packages there.

    cd /path/of/api-maker
    npm install puppeteer
    
  3. Set runOnNativeProcess: true in the configuration of the custom API, and use it :

Custom API : HTML to PDF with Puppeteer
import * as T from 'types';
import { join } from 'path';
const puppeteer = require('puppeteer');

async function main(g: T.IAMGlobal) {
    const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] });
    const page = await browser.newPage();
    await page.setContent('<p>Hello, this paragraph is converted to a PDF using Puppeteer.</p>', { waitUntil: 'load' });
    await page.pdf({ path: join(__dirname, 'uploads', 'puppeteer.pdf'), format: 'A4', printBackground: true });
    await browser.close();

    // uploads/puppeteer.pdf is on the server : upload it to a storage provider or send it as a download.
    return 'ok';
}

module.exports = main;

Good to know

  • Write the files you want to send into the uploads folder and return __am__downloadFilePath : see Download file. Upload them to a storage service instead when they must be kept.
  • Close the browser in every path of the code, errors included : an open browser keeps its memory.
  • The native process runs TypeScript with the node of the server : Node.js must be installed there, on every server.
  • Code on the native process is not isolated from the server : prefer the sandbox when you can, and keep the native process for the APIs which need it.