HTML to PDF on ARM64 Linux (AWS Graviton) in C#

ARM64 servers such as AWS Graviton run .NET at a lower price per hour. SelectPdf.Universal has a native engine package for 64-bit ARM Linux: reference SelectPdf.Universal.Native.linux-arm64 instead of linux-x64, and the same code and the same Dockerfile run on ARM64.

Compiled and run: Linux ARM64 · Linux x64 · macOS ARM64 · Windows x64

Video walkthrough · 3:39 · Watch on YouTube · Short · 1:09
§STEP BY STEP

Build it step by step.

  1. 1Install the NuGet packages

    SelectPdf.Universal is the library. The native package carries the Chromium engine for one platform: add the one for the platform you run on.

    terminal
    dotnet add package SelectPdf.Universal
    dotnet add package SelectPdf.Universal.Native.win-x64
    terminal
    dotnet add package SelectPdf.Universal
    dotnet add package SelectPdf.Universal.Native.linux-x64
    terminal
    dotnet add package SelectPdf.Universal
    dotnet add package SelectPdf.Universal.Native.linux-arm64
    terminal
    dotnet add package SelectPdf.Universal
    dotnet add package SelectPdf.Universal.Native.osx-arm64
  2. 2Create a console app

    Any .NET 8 or .NET 10 project works. To follow along, create a console app, run the install commands above inside its folder, then replace Program.cs with the code below.

    terminal
    dotnet new console -n PdfSample
    cd PdfSample
  3. 3Import the namespaces

    Program.cs · lines 1–2
    using System.Runtime.InteropServices;
    using SelectPdf.Universal;

    System.Runtime.InteropServices reports the architecture; the converter is in SelectPdf.Universal. The project references SelectPdf.Universal.Native.linux-arm64, the ARM64 engine, next to SelectPdf.Universal.

  4. 4Check where it runs

    Program.cs · lines 4–5
    // 1. Where the program runs: Arm64 on AWS Graviton and other ARM64 Linux hosts.
    Console.WriteLine($"{RuntimeInformation.OSDescription}, {RuntimeInformation.ProcessArchitecture}");

    RuntimeInformation.ProcessArchitecture prints Arm64 on a Graviton instance, an ARM64 container or an ARM64 developer machine. It is only there to show the architecture; the conversion does not need it.

  5. 5Prepare the HTML

    Program.cs · lines 7–16
    // 2. The same code as on x64: only the engine package differs.
    string html = """
        <html>
          <body style="font-family: sans-serif">
            <h1>Shipping label #58213</h1>
            <p>Deliver to: Fabrikam Ltd, 12 King Street, London</p>
            <p>Rendered by SelectPdf.Universal on Linux.</p>
          </body>
        </html>
        """;

    A raw string literal holds the page. Any HTML works: a template with your data filled in, a Razor view rendered to a string, or a full page with CSS.

  6. 6Convert and save

    Program.cs · lines 18–21
    HtmlToPdf converter = new HtmlToPdf();
    PdfDocument doc = converter.ConvertHtmlString(html);
    doc.Save("label.pdf");
    doc.Close();

    ConvertHtmlString returns a PdfDocument, Save writes label.pdf and Close releases it. This is the code that runs on x64; on ARM64 only the engine package differs.

  7. 7Run it

    terminal
    dotnet run
    
    # or in a container on an ARM64 machine (writes label.pdf to ./out):
    docker build --platform linux/arm64 -t label-pdf .
    docker run --rm --platform linux/arm64 -v "$PWD/out:/out" -w /out label-pdf

    The program writes label.pdf to the working folder. Without a license key, every page carries a trial watermark; set GlobalProperties.LicenseKey before the first conversion to remove it.

§COMPLETE PROGRAM

The complete Program.cs.

This is the exact file our samples pipeline compiled and ran on Linux ARM64 and Linux x64 and macOS ARM64 and Windows x64 to produce the output on this page.

Program.cs
using System.Runtime.InteropServices;
using SelectPdf.Universal;

// 1. Where the program runs: Arm64 on AWS Graviton and other ARM64 Linux hosts.
Console.WriteLine($"{RuntimeInformation.OSDescription}, {RuntimeInformation.ProcessArchitecture}");

// 2. The same code as on x64: only the engine package differs.
string html = """
    <html>
      <body style="font-family: sans-serif">
        <h1>Shipping label #58213</h1>
        <p>Deliver to: Fabrikam Ltd, 12 King Street, London</p>
        <p>Rendered by SelectPdf.Universal on Linux.</p>
      </body>
    </html>
    """;

HtmlToPdf converter = new HtmlToPdf();
PdfDocument doc = converter.ConvertHtmlString(html);
doc.Save("label.pdf");
doc.Close();

Open the project on GitHub → this program and the variations below, ready to dotnet run

§SUPPORTING FILES

Supporting files.

Dockerfile

The same two stages as on x64. The .NET base images are multi-architecture, so on an ARM64 machine, or when building with --platform linux/arm64, they come in their ARM64 variants. There is no apt-get line: the ARM64 engine package carries everything the engine needs.

Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -f net10.0 -o /out

FROM mcr.microsoft.com/dotnet/runtime:10.0
# no apt-get: the Chromium closure ships in SelectPdf.Universal.Native.linux-arm64
WORKDIR /app
COPY --from=build /out ./
ENTRYPOINT ["dotnet", "/app/PdfSample.dll"]

.dockerignore

Keeps the host's build output out of the image's build context, so the publish inside the container starts clean.

.dockerignore
bin/
obj/
out/
§OUTPUT

The PDF it produces.

First page of label.pdf, the PDF this sample produces
label.pdf, page 1

Download label.pdf ↓ the file this sample produced, unedited

§TROUBLESHOOTING

Common problems and fixes.

The engine fails to start on ARM64

The image probably holds the x64 engine. Reference SelectPdf.Universal.Native.linux-arm64, not linux-x64, and reference only one engine package per build: they unpack into the same folder.

Building on an Apple Silicon Mac

Docker Desktop for Mac runs Linux containers, so the image needs SelectPdf.Universal.Native.linux-arm64. The osx-arm64 package is for applications that run natively on macOS.

Text in other scripts shows as boxes

The Linux base images have no fonts for CJK, Arabic and many other scripts. Reference SelectPdf.Universal.Fonts; it works the same on ARM64.

The engine does not start on an x64 machine running the ARM64 image

Docker can build an ARM64 image on an x64 machine with --platform linux/arm64, but under its emulation the Chromium engine stops with "GPU process isn't usable". Run ARM64 images on an ARM64 host: a Graviton instance, an ARM64 build agent or an Apple Silicon Mac with Docker Desktop.

§GOOD TO KNOW

Good to know.

  • SelectPdf.Universal.Native.linux-arm64 holds the native engines for 64-bit Linux on ARM64 (aarch64). Source
  • For ARM64 containers, reference SelectPdf.Universal.Native.linux-arm64 instead of linux-x64; everything else stays the same, and the multi-architecture .NET base images select the matching architecture. Source
  • SelectPdf runs on Amazon Web Services on Linux x64 and arm64 (Graviton); on Linux, reference linux-arm64 for Graviton. Source
  • On AWS Lambda, arm64 is cheaper per second and converts at a similar speed. Source
?FAQ

Questions, answered.

Short answers about this guide. The SelectPdf.Universal documentation covers every class and option.

Yes. Reference SelectPdf.Universal with SelectPdf.Universal.Native.linux-arm64, the native engine package for 64-bit ARM Linux. The code is the same as on x64.
Yes. On Graviton, reference the linux-arm64 package, on EC2, in containers, and in AWS Lambda functions created with --architectures arm64.
No. The linux-arm64 package carries the engine and every library it needs, so the Dockerfile has no apt-get step.
osx-arm64 for an application that runs on macOS itself, and linux-arm64 for a Linux container you run with Docker Desktop on the Mac.