HTML to PDF on Google Cloud Run in C#

Cloud Run runs container images and scales them with the traffic. An ASP.NET Core app with SelectPdf.Universal runs there in an image built from the standard .NET aspnet image, as its non-root user, with nothing to install: the native package carries the engine and the fonts package covers every script.

Compiled and run: Cloud Run image (aspnet:10.0, non-root user), run locally, x64 · Linux ARM64 · Linux x64 · macOS ARM64 · Windows x64

Video walkthrough · 4:01 · Watch on YouTube · Short · 1:04
§STEP BY STEP

Build it step by step.

  1. 1Create the project

    A web project with three packages and a two-stage Dockerfile. The engine package matches the machine that builds the app, so dotnet run works on Windows, and dotnet publish -r linux-x64 in the Dockerfile packs the Linux engine. The fonts package covers Chinese, Japanese, Korean, Arabic and other scripts the Linux image has no fonts for.

    PdfApp.csproj
    <Project Sdk="Microsoft.NET.Sdk.Web">
    
      <PropertyGroup>
        <TargetFramework>net10.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
      </PropertyGroup>
    
      <!-- the engine for the machine that builds the app; publishing with -r linux-x64 picks Linux,
           which is what the Cloud Run image needs. Fonts: the Linux image has none for CJK and other scripts. -->
      <PropertyGroup>
        <_SelectPdfArch>$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant())</_SelectPdfArch>
        <SelectPdfRid Condition="$([MSBuild]::IsOSPlatform('Windows'))">win-$(_SelectPdfArch)</SelectPdfRid>
        <SelectPdfRid Condition="$([MSBuild]::IsOSPlatform('Linux'))">linux-$(_SelectPdfArch)</SelectPdfRid>
        <SelectPdfRid Condition="$([MSBuild]::IsOSPlatform('OSX'))">osx-$(_SelectPdfArch)</SelectPdfRid>
        <SelectPdfRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier)</SelectPdfRid>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="SelectPdf.Universal" Version="26.5.0" />
        <PackageReference Include="SelectPdf.Universal.Native.$(SelectPdfRid)" Version="26.5.0" />
        <PackageReference Include="SelectPdf.Universal.Fonts" Version="26.5.0" />
      </ItemGroup>
    
    </Project>
    Dockerfile
    FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
    WORKDIR /src
    COPY . .
    RUN dotnet publish PdfApp.csproj -c Release -r linux-x64 --self-contained false -o /app
    
    FROM mcr.microsoft.com/dotnet/aspnet:10.0
    WORKDIR /app
    COPY --from=build /app ./
    # the image's non-root user; the engine needs no extra packages
    USER $APP_UID
    ENTRYPOINT ["dotnet", "PdfApp.dll"]
  2. 2A minimal web app on Cloud Run's port

    Program.cs · lines 1–8
    using SelectPdf.Universal;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // on Cloud Run, listen on the port it passes in PORT (8080 by default)
    string? port = Environment.GetEnvironmentVariable("PORT");
    if (port != null) builder.WebHost.UseUrls($"http://0.0.0.0:{port}");
    var app = builder.Build();

    Cloud Run tells the container which port to listen on in the PORT environment variable, 8080 by default; when it is set, UseUrls binds to it on all interfaces. Run locally, the app keeps the usual ASP.NET Core address.

  3. 3HTML in several scripts

    Program.cs · lines 10–17
    // GET /report.pdf: a PDF made on the server, with text in several scripts
    app.MapGet("/report.pdf", async (CancellationToken cancel) =>
    {
        string html = """
            <h1>Monthly report</h1>
            <p>English · Deutsch · Ελληνικά · Русский</p>
            <p>日本語 · 中文 · 한국어 · العربية</p>
            """;

    One endpoint, GET /report.pdf. The Japanese, Chinese, Korean and Arabic text needs SelectPdf.Universal.Fonts: the Linux image has no fonts for those scripts, and without the package they print as empty boxes.

  4. 4Convert and return the PDF

    Program.cs · lines 19–27
        HtmlToPdf converter = new HtmlToPdf();
        PdfDocument doc = await converter.ConvertHtmlStringAsync(html, cancel);
        byte[] pdf = doc.Save();
        doc.Close();
    
        return Results.File(pdf, "application/pdf");
    });
    
    app.Run();

    ConvertHtmlStringAsync renders the HTML with the request's CancellationToken, and Save() returns the bytes. Results.File sends them as application/pdf.

  5. 5Run it

    terminal
    dotnet run
    
    # then open the address dotnet run prints, followed by /report.pdf

    The request returns report.pdf. 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 Cloud Run image (aspnet:10.0, non-root user), run locally, x64 and Linux ARM64 and Linux x64 and macOS ARM64 and Windows x64 to produce the output on this page.

Program.cs
using SelectPdf.Universal;

var builder = WebApplication.CreateBuilder(args);

// on Cloud Run, listen on the port it passes in PORT (8080 by default)
string? port = Environment.GetEnvironmentVariable("PORT");
if (port != null) builder.WebHost.UseUrls($"http://0.0.0.0:{port}");
var app = builder.Build();

// GET /report.pdf: a PDF made on the server, with text in several scripts
app.MapGet("/report.pdf", async (CancellationToken cancel) =>
{
    string html = """
        <h1>Monthly report</h1>
        <p>English · Deutsch · Ελληνικά · Русский</p>
        <p>日本語 · 中文 · 한국어 · العربية</p>
        """;

    HtmlToPdf converter = new HtmlToPdf();
    PdfDocument doc = await converter.ConvertHtmlStringAsync(html, cancel);
    byte[] pdf = doc.Save();
    doc.Close();

    return Results.File(pdf, "application/pdf");
});

app.Run();

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

§DEPLOY

Deploy it.

Set up the project once, build and push the image to Artifact Registry, then deploy. Replace YOUR_PROJECT_ID with your project and pick your region; --no-allow-unauthenticated means callers need an identity token, so use --allow-unauthenticated only for a public service.

terminal
gcloud auth login
gcloud config set project YOUR_PROJECT_ID
gcloud config set run/region europe-north1
gcloud services enable run.googleapis.com artifactregistry.googleapis.com
gcloud artifacts repositories create selectpdf --repository-format=docker --location=europe-north1
gcloud auth configure-docker europe-north1-docker.pkg.dev

docker build --platform linux/amd64 -t europe-north1-docker.pkg.dev/YOUR_PROJECT_ID/selectpdf/myapp .
docker push europe-north1-docker.pkg.dev/YOUR_PROJECT_ID/selectpdf/myapp

gcloud run deploy myapp \
  --image europe-north1-docker.pkg.dev/YOUR_PROJECT_ID/selectpdf/myapp \
  --cpu 2 --memory 4Gi --timeout 300 --concurrency 8 \
  --no-allow-unauthenticated

These steps, with the Windows variants, are in the documentation: selectpdf.com/pdf-library/html/Google-Cloud-Run.htm.

§OUTPUT

The PDF it produces.

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

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

§TROUBLESHOOTING

Common problems and fixes.

HTTP 503 and a restarted instance

The instance ran out of memory on a heavy page. Deploy with --memory 4Gi and --cpu 2.

The first request is slow

The first conversion of a new instance takes 5 to 10 seconds; a warm one converts a page like selectpdf.com in about 1.4 seconds. Cloud Run keeps an idle instance for a while, and --min-instances 1 keeps one ready.

Characters print as boxes

Reference SelectPdf.Universal.Fonts: the Linux image has no fonts for CJK, Arabic and many other scripts. The package copies them next to the app, so the Dockerfile needs no apt-get.

The container does not answer on Cloud Run

Listen on the port in PORT, on all interfaces (0.0.0.0), as the sample does. Build the image for linux/amd64 when you build on an ARM machine.

§GOOD TO KNOW

Good to know.

  • SelectPdf runs on Cloud Run in a Linux image built from the standard mcr.microsoft.com/dotnet/aspnet image, as its non-root user, in both execution environments, and also as a Cloud Run function. Source
  • Cloud Run passes the port in the PORT environment variable, 8080 by default. Source
  • Deploy with --cpu 2 --memory 4Gi: with 2 GiB a heavy page can run the instance out of memory, and Cloud Run then restarts it and the request fails with HTTP 503. Source
  • --concurrency 8 matches the number of pages SelectPdf converts at once by default. Source
  • Measured in October 2026 (europe-north1, 2 vCPU): a warm instance converts selectpdf.com in about 1.4 seconds; the first conversion of a new instance takes 5 to 10 seconds. Source
?FAQ

Questions, answered.

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

Yes. Reference SelectPdf.Universal, SelectPdf.Universal.Native.linux-x64 and SelectPdf.Universal.Fonts in an ASP.NET Core app, build an image from mcr.microsoft.com/dotnet/aspnet and deploy it with gcloud run deploy.
No. The native package carries the engine and every library it needs, and the fonts package covers every script, so the Dockerfile has no apt-get step and runs as the image's non-root user.
--cpu 2 --memory 4Gi, with --concurrency 8 to match the number of pages converted at once by default. With 2 GiB a heavy page can run the instance out of memory.
Yes. The same code runs as a Cloud Run function (gcloud functions deploy --gen2 with the Google.Cloud.Functions.Hosting package), with at least --memory 2Gi and --cpu 2.