Running Puppeteer-Sharp in Azure Container Apps

June 10, 2023

Some time ago, I encountered difficulties using Puppeteer-Sharp in an Azure Function consumption plan to generate PDFs based on an HTTP trigger. Although the functionality was functional, the cold starts were terrible due to the download of Chromium (over 150MB) into temporary storage.

Fortunately, I came across a helpful blog post by Darío Kondratiuk, the maintainer of Puppeteer-Sharp. He explained a solution using Docker images with Chromium included, which eliminated the dreadful cold starts. However, please note that this approach only works for premium plans, which are more expensive than consumption plans.

Inspired by Dario's approach, I successfully used Puppeteer-Sharp within an Azure Container App. Azure Container Apps, similar to the consumption plan for Azure Functions, can scale down to 0 instances, meaning you won't incur any costs when idle.

Costs and performance

Let's begin by discussing the costs and performance aspect. Here are some statistics from an average day that can help you determine if utilizing Azure Container Apps for running Puppeteer-Sharp aligns with your PDF generation requirements.

Average day
Requests 510
Response times 877ms
Min. / max. scale 0-1
CPU cores / memory 2 / 4Gi *
Costs € 2.53,-

*We did not yet run tests to lower the CPU and/or memory.

How to?

Before getting started, make sure you have an Azure Container App up and running. If you don't know how to create one, you can follow the instructions provided in this guide.

Step 1: Create an ASP.NET Application

Start by creating a new ASP.NET Core project and adding the Puppeteer-Sharp NuGet package using the following command:

dotnet add package PuppeteerSharp

Next, add a minimal API endpoint for generating a PDF. Here's an example code snippet:

using PuppeteerSharp;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/api/generatepdf", async (string? name) =>
{
    var launchOptions = new LaunchOptions { Headless = true };

    await using var browser = await Puppeteer.LaunchAsync(launchOptions);
    await using var page = await browser.NewPageAsync();

    await page.SetContentAsync($"Hello, {name ?? "stranger"}!");

    var pdf = await page.PdfStreamAsync();

    return pdf == null
        ? Results.NotFound()
        : Results.File(pdf, "application/pdf", "file.pdf");
});

app.Run();

To support containerization, create a Dockerfile in the root directory of your project with the following content:

FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

# Install dependencies required by Chromium
RUN apt-get update && apt-get install -y wget libxss1 libgconf-2-4 gnupg2
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome-keyring.gpg
RUN echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome-keyring.gpg] https://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list > /dev/null
RUN apt-get update && apt-get install -y google-chrome-stable

# Set an environment variable to point Puppeteer to the installed Chromium
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable

FROM base AS final
WORKDIR /app
COPY . .
ENTRYPOINT ["dotnet", "UI.PdfGenerator.dll"] # Update the name with your own project name

Step 2: Build and push the image

Assuming you're using an Azure Container Registry as the repository for your containers, follow these steps to build and push the image:

dotnet publish \
    --configuration Release \
    --output ./UI

az acr build \
    --resource-group "rg-pdfgeneration" \
    --registry "crcommon" \
    --image "pdfs:latest" "./UI"

You can follow the instructions on this page to learn how to push your image to the Container App.

Step 3: Test it!

Go to your Azure Container App and hit the following endpoint:

https://[PERSONAL].azurecontainerapps.io/api/generatepdf?name=Maikel