Skip to main content
ModPageSpeed 2.0: AVIF, WebP, and critical CSS — up to 69% less page weight on the live demo
2.0 1.15

ASP.NET Core Sidecar (NuGet)

Add mod_pagespeed 1.15 to an ASP.NET Core app with two lines of middleware. WeAmp.PageSpeed.Sidecar runs a bundled nginx + ngx_pagespeed optimizer on loopback behind your app. Linux-only (linux-x64, linux-arm64), with AddPageSpeed()/UsePageSpeed() wiring and a full options reference.

WeAmp.PageSpeed.Sidecar adds mod_pagespeed 1.15 to your ASP.NET Core app with two lines of middleware. Your Kestrel app stays the public front door; a bundled, matched (nginx + ngx_pagespeed) optimizer runs on loopback behind it, recompressing images (jpeg/png/webp), minifying CSS and JS, inlining and extracting critical CSS, and rewriting HTML for Core Web Vitals on the way out. Requests that can’t be optimized — and any request while the optimizer isn’t running — pass straight through your app un-optimized.

builder.Services.AddPageSpeed(builder.Configuration);
// ...
app.UsePageSpeed();

That’s the whole setup. No domain list, no separate front-proxy to configure. This is the default Inverse topology; a classic front-proxy mode (bundled nginx as the public front door) is also available — see Sidecar modes.

How it works

In the default Inverse mode, your ASP.NET Core app is the public front door. When the UsePageSpeed() middleware runs, it streams an optimizable response over loopback to the bundled nginx, which applies the ngx_pagespeed optimizations and proxies back to a second, private raw-origin Kestrel endpoint where the middleware bypasses itself. The loop break is automatic and internal. Non-optimizable requests, and any request received while the sidecar isn’t running, pass straight through un-optimized — optimization is always additive.

The package binds the public port (Sidecar.ListenPort, 8080 by default), generates its own nginx.conf, manages the nginx process lifecycle (start, health, auto-restart, graceful stop), and registers an ASP.NET Core health check. It serves plain HTTP; for HTTPS, front it with a TLS terminator or load balancer (or set Sidecar.OwnPublicPort = false and configure your own endpoint via Kestrel:Endpoints).

Install

dotnet add package WeAmp.PageSpeed.Sidecar --prerelease

This automatically pulls the matched native engine package (WeAmp.PageSpeed.Sidecar.NativeAssets.Linux, which bundles the nginx + ngx_pagespeed binaries for linux-x64 and linux-arm64). Publish with a Linux runtime identifier so the bundled nginx ships next to your app — a framework-dependent publish with no -r omits the nginx binary and the sidecar fails to start:

dotnet publish -c Release -r linux-x64    # or: -r linux-arm64

Wire it up

var builder = WebApplication.CreateBuilder(args);

// Reads the "PageSpeed" configuration section. Mode defaults to Inverse; the package
// binds the public port (Sidecar.ListenPort, 8080) and runs the optimizer behind it.
builder.Services.AddPageSpeed(builder.Configuration);

var app = builder.Build();
app.UsePageSpeed();
app.MapHealthChecks("/health");
app.Run();

Or configure in code:

builder.Services.AddPageSpeed(options =>
{
    options.RewriteLevel = "CoreFilters";
    options.LicenseKey = builder.Configuration["PageSpeed:LicenseKey"]; // BYOL, from a secret
});

You do not list domains to make optimization work: by default the optimizer rewrites same-origin resources for any host your app serves, with zero configuration (localhost included, for local dev). Domains.AuthorizedDomains is only for the advanced cases below.

AddPageSpeed registers a health check named pagespeed (tagged pagespeed, sidecar), so the app.MapHealthChecks("/health") endpoint above reflects bundled-nginx liveness. For a PageSpeed-only endpoint, call app.MapPageSpeedHealthCheck("/health/pagespeed") (it filters on the pagespeed tag).

Configuration options

Bind a PageSpeed section in appsettings.json. Every key is optional and falls back to its default (the defaults below are the Inverse defaults):

{
  "PageSpeed": {
    "Enabled": true,
    "RewriteLevel": "CoreFilters",
    "Sidecar": {
      "OriginPort": 0
    },
    "Cache": {
      "FileCachePath": "/var/cache/pagespeed",
      "FileCacheSizeKb": 10240000
    },
    "AdminAuth": { "Enabled": true }
  }
}
KeyDefaultNotes
EnabledtrueMaster switch for optimization.
RewriteLevelCoreFiltersOne of PassThrough, CoreFilters, OptimizeForBandwidth, MobilizeFilters, TestingCoreFilters, AllFilters.
Sidecar.ModeInverseInverse (default — your middleware is the public front door, nginx optimizes on loopback), Process (classic front-proxy — bundled nginx is the public front door, Kestrel the private origin), or External (operator-managed nginx). Docker is reserved and fails config validation.
Sidecar.ListenPort8080The public port the package binds in Inverse (the default) and the public nginx port in Process.
Sidecar.OwnPublicPorttrueInverse only. When true (default), the package binds the public endpoint on 0.0.0.0:Sidecar.ListenPort. Set false to own the bind yourself via Kestrel:Endpoints (--urls/UseUrls/ASPNETCORE_URLS are overridden once the loopback endpoint is added and won’t bind publicly).
Sidecar.OriginPort0Private raw-origin loopback port. 0 = auto. In Inverse it is always loopback-TCP (never a Unix socket); in Process/External, 0 prefers a private Unix-domain socket. You normally leave this at 0.
Sidecar.NginxLoopbackPort0Inverse only. The loopback-only port nginx listens on. 0 = auto.
Sidecar.RestrictToAuthorizedHostsfalseInverse only, defense-in-depth. When true, optimizes only hosts in Domains.AuthorizedDomains (loopback always allowed); every other host is served un-optimized. Default false optimizes all hosts (forward-all).
Sidecar.AllowPublicAdminfalseInverse only. When true, exposes the /pagespeed_* admin endpoints from the public front door; requires AdminAuth.Enabled. Default false returns 404 for them from the public endpoint.
Sidecar.UseLaunchShimfalseOpt in to the native orphan-on-hard-kill launch shim. Off by default; normal shutdown stops nginx gracefully.
Domains.AuthorizedDomains["localhost", "127.0.0.1"]Hosts authorized for cross-origin rewriting (e.g. a CDN). A request’s own same-origin resources are always authorized without listing the host here, so this is not required for normal optimization; it is the strict allowlist only when Sidecar.RestrictToAuthorizedHosts = true.
Cache.FileCachePathsystem temp dirOn-disk cache location for optimized resources.
Cache.FileCacheSizeKb10240000 (10 GB)File-cache size cap, in KB.
AdminAuth.EnabledtrueGates the /pagespeed_* admin endpoints behind a bearer token. Required to be true if Sidecar.AllowPublicAdmin = true.

Source LicenseKey and AdminAuth.Token from a secret store or environment variable — do not put them in appsettings.json.

Container & runtime requirements

The bundled nginx + module are glibc ELF binaries built for a glibc ≥ 2.31 floor, so they run on Debian 11/12, Ubuntu 20.04+, RHEL/Alma 8/9, and Amazon Linux 2023. libstdc++ is statically linked and the build has no OpenSSL dependency, so there is no libstdc++ or libssl/libcrypto host requirement. The binaries link a small set of host shared libraries:

Host libraryProvided byNotes
libpcre.so.3libpcre3nginx PCRE — missing from -chiseled/-distroless images
libz.so.1zlib1gpresent on standard .NET images
libcrypt.so.1libcrypt1present on standard .NET images
  • Recommended base image: mcr.microsoft.com/dotnet/aspnet:8.0 (Debian); add the one missing lib with apt-get install -y libpcre3.
  • Not supported: -chiseled / -distroless .NET images (no libpcre3) and Alpine/musl images (the binaries are glibc-linked). Use a Debian/Ubuntu glibc base. nginx still runs in Inverse mode — on loopback — so the same base-image requirements apply.

Launch your app as dotnet YourApp.dll (the default ENTRYPOINT for dotnet publish container tooling and mcr.microsoft.com/dotnet/aspnet images) so SIGTERM from docker stop / Kubernetes / systemd reaches the host’s graceful shutdown and stops the bundled nginx cleanly. Launching the apphost directly (./YourApp) does not reliably propagate SIGTERM, so the bundled nginx can survive shutdown and keep holding its loopback port.

Verify it’s working

curl -sI http://localhost:8080/ | grep -i x-page-speed

A present X-Page-Speed header confirms the optimizer is in the path. During evaluation you will also see X-PageSpeed-Warn: unlicensed — that is expected, not an error; set PageSpeed:LicenseKey to clear it. For per-filter rewrite counts, check /pagespeed_statistics (loopback-only by default; the middleware returns 404 for the /pagespeed_* admin paths from the public front door unless Sidecar.AllowPublicAdmin is enabled).

Licensing

The product is commercially licensed under the Business Source License 1.1 (BUSL-1.1). Optimization runs unlicensed for evaluation, emitting an X-PageSpeed-Warn: unlicensed header; a commercial license is required for production. Provide your license token via PageSpeed:LicenseKey (BYOL) to clear the warning. Under BUSL-1.1, each version becomes available under the Apache License 2.0 four years after its first public release. See Downloads & Licensing and plans and pricing.

When to use the sidecar vs. ModPageSpeed 2.0

WeAmp.PageSpeed.Sidecar (1.15)WeAmp.PageSpeed.AspNetCore (2.0)
ModelMiddleware front door; bundled nginx optimizes on loopbackIn-process middleware (P/Invoke)
PlatformsLinux only (linux-x64, linux-arm64)linux-x64/arm64, osx-arm64, win-x64
Image formatsjpeg/png/webp recompressionWebP and AVIF transcoding
Best fitThe proven 1.x nginx engine on LinuxCross-platform, in-process, newest pipeline

Both run optimization unlicensed for evaluation and require a commercial license for production. See which edition fits.