§HOW-TO GUIDE · C# · .NET 8 & 10

HTML to PDF in Docker on Linux with C# (no apt-get)

SelectPdf.Universal ships its Chromium engine, with every shared library it needs, inside a per-platform NuGet package, so a stock .NET runtime image converts HTML to PDF with nothing else installed.

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

Video walkthrough · 2:28 · Watch on YouTube · Short · 0:40
§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. 3Prepare the HTML

    Program.cs · lines 3–10
    string html = """
        <html>
          <body style="font-family: sans-serif">
            <h1>Invoice #1042</h1>
            <p>Rendered on Linux by SelectPdf.Universal.</p>
          </body>
        </html>
        """;

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

  4. 4Convert and save

    Program.cs · lines 12–15
    HtmlToPdf converter = new HtmlToPdf();
    PdfDocument doc = converter.ConvertHtmlString(html);
    doc.Save("invoice.pdf");
    doc.Close();

    HtmlToPdf.ConvertHtmlString returns a PdfDocument. Save writes invoice.pdf and Close releases it. The same code runs unchanged on Windows, Linux and macOS; only the native package differs.

  5. 5Run it

    terminal
    dotnet run
    
    # or in the container (writes invoice.pdf to ./out):
    docker build -t invoice-pdf .
    docker run --rm -v "$PWD/out:/out" -w /out invoice-pdf

    The program writes invoice.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 SelectPdf.Universal;

string html = """
    <html>
      <body style="font-family: sans-serif">
        <h1>Invoice #1042</h1>
        <p>Rendered on Linux by SelectPdf.Universal.</p>
      </body>
    </html>
    """;

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

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

§DOCKER

Build and run the container.

Dockerfile

Two stages: the SDK image publishes the app, and the stock runtime image runs it. There is no apt-get line: everything the engine needs comes with the native NuGet package.

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-noble
# no apt-get: the Chromium closure ships in SelectPdf.Universal.Native.linux-x64
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 and the context stays small.

.dockerignore
bin/
obj/
out/
§OUTPUT

The PDF it produces.

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

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

§TROUBLESHOOTING

Common problems and fixes.

Alpine images

Alpine uses musl, and the native engines this app runs need glibc, so keep the default Debian or Ubuntu based mcr.microsoft.com/dotnet images here. An app on Alpine converts through the remote engine instead: the ghcr.io/selectpdf/engine container renders the pages, and the app references only SelectPdf.Universal. The Alpine Linux guide under Related guides shows the setup.

Chinese, Japanese, Arabic or other scripts show as boxes

The .NET base images have no fonts, and the fonts built into the library cover Latin, Greek and Cyrillic. Add the SelectPdf.Universal.Fonts package for every other script.

Running as a non-root user

Nothing extra is needed when the app is published inside the Docker build: add USER app before the entry point. The native package makes the engine readable and executable for every user.

Sizing the container

Plan for at least 1 core and 2 GB of RAM per container, and prefer 2 or more cores for consistently fast conversions under load.

§GOOD TO KNOW

Good to know.

  • The Linux native package bundles the complete Chromium shared-library closure, so there is no apt-get step. Source
  • A plain docker run works: no --no-sandbox, no added capabilities, no privileged mode, no --shm-size tuning. Source
  • Verified on Debian 12, Ubuntu 22.04 and 24.04, and Amazon Linux 2023; the engine needs glibc 2.30 or later. Source
  • Linux x64 and ARM64 are both supported; use SelectPdf.Universal.Native.linux-arm64 on ARM. Source
?FAQ

Questions, answered.

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

No. The SelectPdf.Universal.Native.linux-x64 package bundles the complete Chromium shared-library closure. A stock mcr.microsoft.com/dotnet runtime or aspnet image works with no apt-get step.
No. A plain docker run works: no special flags, no extra capabilities and no shared-memory tuning.
Yes. Reference SelectPdf.Universal.Native.linux-arm64 instead of linux-x64. The .NET base images are multi-architecture, so the Dockerfile stays the same.
For an app that runs the engines itself, any glibc-based image with glibc 2.30 or later. It is verified on Debian 12, Ubuntu 22.04 and 24.04, and Amazon Linux 2023. Alpine (musl) images cannot run the native engines; an app on Alpine converts through the remote engine container, ghcr.io/selectpdf/engine, instead.