← All docs

Deployment & Self-Hosting

DynDyn.Pdf is a self-contained .NET 10 codebase with no external services, no database, and no telemetry — everything (extraction, font synthesis, HTML composition) runs in-process, and the native rendering library is restored from NuGet for the target RID. That makes it straightforward to run on-prem, in a locked-down container, or air-gapped.

There are two deployable surfaces:

Surface Project Use it for
CLI (dyndyn-pdf) src/DynDyn.Pdf.Cli Batch/one-shot conversion, cron/pipeline jobs, scripting
Web service src/DynDyn.Pdf.Web An HTTP upload + convert + download API (browser front-end included)

Both link the same DynDyn.Pdf.Core engine, so output is identical regardless of how you invoke it.


1. Run the CLI

Build & publish

# Framework-dependent build (needs the .NET 10 runtime on the host)
dotnet build -c Release

# Self-contained, single-file publish for a specific RID (no runtime install needed on the host)
dotnet publish src/DynDyn.Pdf.Cli -c Release -r linux-x64   --self-contained \
  -p:PublishSingleFile=true -o ./publish/linux-x64
dotnet publish src/DynDyn.Pdf.Cli -c Release -r win-x64     --self-contained \
  -p:PublishSingleFile=true -o ./publish/win-x64

The correct native rendering library for the publish RID is auto-restored from NuGet per RID and copied automatically (no manual install). Supported RIDs: win-x64/arm64/x86, linux-x64/arm64 (incl. musl/Alpine), osx-x64/arm64. No apt install step.

Invoke

# One document → one self-contained HTML file
dyndyn-pdf convert report.pdf -o out/

# A whole folder, 8 documents at a time, per-page output
dyndyn-pdf convert reports/ -o out/ --format pages --jobs 8

# A glob, selected pages, viewer app packaging
dyndyn-pdf convert "reports/**/*.pdf" -o out/ --format viewer --pages 1-20

# Sanity-check the native stack / inspect a document without converting
dyndyn-pdf info report.pdf

Exit codes (script against these): 0 success, 1 conversion failed, 2 invalid arguments, 3 input not found, 4 a native-only feature was requested but the native rendering library is unavailable for this RID (use --engine managed with an SVG background, the default, which needs no native lib), 5 the conversion completed but tripped --fail-on, 6 the PDF is password-protected and no password (or the wrong one) was supplied via --password, 7 the conversion was aborted by --timeout, 8 the PDF is encrypted with a security handler this engine does not implement (e.g. /Adobe.PubSec public-key encryption) — no password can unlock it. Full table in configuration.md.

See configuration.md for every flag and api.md for the library API.


2. Run the Web service

The service is a minimal-API ASP.NET Core app. It accepts a PDF upload, converts it on a background task, streams progress over Server-Sent Events, and serves the result for download/preview. A simple upload page ships in wwwroot/index.html.

# Development
dotnet run --project src/DynDyn.Pdf.Web

# Production publish
dotnet publish src/DynDyn.Pdf.Web -c Release -r linux-x64 --self-contained -o ./publish/web
./publish/web/DynDyn.Pdf.Web --urls http://0.0.0.0:8080

Endpoints (full contract in api.md):

Method Route Purpose
POST /api/jobs Upload a PDF (multipart/form-data) + options → { jobId, token }
GET /api/jobs/{id}/events?token=… SSE stream: progress events, then done/error
DELETE /api/jobs/{id}?token=… Cancel an in-flight/queued job and free its concurrency slot early
GET /api/jobs/{id}/download?token=… Download the result (HTML, or a .zip for per-page output)
GET /api/jobs/{id}/preview?token=… Inline HTML preview (single/viewer formats)
GET / Bundled upload UI (static files from wwwroot)

Every route above except POST /api/jobs requires the per-job ownership token returned by POST /api/jobs — it is returned exactly once, and every other request for that job answers 403 if the token is missing or doesn't match. See api.md for why this exists: it is what stops one caller from reading, downloading, or cancelling another caller's job by guessing or observing a jobId, which matters most for open/anonymous deployments where many callers share one API key or none at all.

The service enables Server GC and concurrent GC (DynDyn.Pdf.Web.csproj) so parallel page extraction isn't serialized by GC contention, and RollForward=Major so it runs on a newer shared framework when the exact 10.0 runtime isn't present.

Operational notes for the current build


3. Container guidance

The native rendering library ships in the NuGet packages, so the runtime image needs no extra apt packages — the stock mcr.microsoft.com/dotnet/aspnet:10.0 image already carries its dependencies (glibc, libstdc++, ICU via the base image). For Alpine, publish with a -musl RID and use the aspnet:10.0-alpine base; the musl native build is restored automatically.

Sample Dockerfile (Web service)

# ---- build ----
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore src/DynDyn.Pdf.Web/DynDyn.Pdf.Web.csproj
RUN dotnet publish src/DynDyn.Pdf.Web/DynDyn.Pdf.Web.csproj \
    -c Release -o /app --no-restore

# ---- runtime ----
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
# The native rendering library is delivered via NuGet and published under runtimes/<rid>/native —
# no apt-get install is required.
COPY --from=build /app .

# Run as a non-root user; the app only writes to its work dir (see below).
RUN useradd -u 10001 -m appuser
USER appuser

ENV ASPNETCORE_URLS=http://0.0.0.0:8080 \
    DOTNET_gcServer=1
EXPOSE 8080

# Job scratch space. Mount an emptyDir/tmpfs here in orchestration; it is transient.
VOLUME ["/tmp"]

ENTRYPOINT ["dotnet", "DynDyn.Pdf.Web.dll"]

For the CLI in a container, swap the entrypoint for ["dotnet", "DynDyn.Pdf.Cli.dll"] (or the self-contained single-file binary) and mount input/output volumes. Because conversion is CPU-bound and stateless, CLI containers are ideal as short-lived batch jobs.

Hardening tips (details in security.md):


4. Configuration reference

The CLI takes all configuration as command-line flags — there is no config file. See configuration.md for the complete flag table with defaults.

The Web service reads standard ASP.NET Core host configuration plus its own DynDynPdf section (appsettings.json, environment variables as DynDynPdf__Setting, or the command line):

Setting Source Default Notes
Listen URL(s) ASPNETCORE_URLS env / --urls http://localhost:5000 Standard Kestrel binding
Environment ASPNETCORE_ENVIRONMENT env Production Standard
Server GC DOTNET_gcServer env / csproj on (csproj) Enabled in-project; keep on
Job scratch root Path.GetTempPath() OS temp %TEMP%/dyndyn-pdf-web
Native library override DYNDYN_PDF_NATIVE_LIB env unset Point at a pinned/patched native rendering library file or directory
DynDynPdf:ApiKeys config empty Comma-separated accepted keys; empty = loopback-only unless AllowAnonymous
DynDynPdf:AllowAnonymous config false Open /api/* to non-loopback callers when no keys are configured
DynDynPdf:MaxConcurrentConversions config 0 (auto) 0/negative derives from cores + available memory
DynDynPdf:MaxLiveJobs config 200 New uploads past this get 503
DynDynPdf:JobTtlMinutes config 30 Idle job (and its temp dir) eviction
DynDynPdf:CleanupIntervalSeconds config 60 Reaper scan interval
DynDynPdf:RateLimitRequestsPerMinute config 120 Per-key/per-IP fixed-window budget on /api/*
DynDynPdf:MaxUploadBytes config 1 GB Kestrel + multipart body cap
DynDynPdf:ConversionTimeoutSeconds config 600 Hard per-job timeout; 0/negative disables
DynDynPdf:MaxPagesPerJob config 0 (unlimited) Hard cap on pages converted per job; cannot be exceeded from the request

Full field docs (defaults, "why") live on DynDynPdfWebOptions itself; see api.md for the exact auth decision table. OpenTelemetry export is opt-in and unrelated to this section — see AddDynDynPdfObservability / standard OTEL_* env vars; until configured, the app logs to stdout (AddSimpleConsole), which your platform can scrape.

Wire any of these through environment variables in your container/orchestrator so no secrets land in the image.


5. Scaling

Within one document

--parallelism N (CLI) / ConversionOptions.MaxDegreeOfParallelism caps the managed post-processing threads (font build, deflate, SVG/HTML composition). Native extraction is inherently single-threaded (the native renderer isn't thread-safe; managed extraction is the bottleneck), so this scales the second half of the pipeline.

Across documents (batch)

--jobs N (CLI) / BatchOptions.MaxConcurrentDocuments converts N PDFs at once, each fully isolated (its own engine/reader/font registry). This is the ~1.35x throughput lever for the managed engine and depends on Server GC being enabled (it is, in the Web project and via DOTNET_gcServer=1). When --parallelism is unset in batch mode, per-document parallelism is derived as max(1, cores / jobs) so jobs × per-doc threads never oversubscribes the CPU.

Horizontal

The Web service today keeps jobs in-process and on local disk, so it does not natively load-balance across replicas: a client must talk to the same instance for the whole upload → events → download lifecycle. To scale out now:

A future revision that externalizes job state (shared object storage + a distributed registry) will make the Web service horizontally scalable without affinity; until then prefer the CLI-as-batch-job pattern for high-volume, multi-node throughput.


6. On-prem & data sovereignty

DynDyn.Pdf is designed to be run entirely inside your own trust boundary:

For regulated / sovereign deployments, run the CLI or Web service in your own datacenter or private cloud, restrict egress at the network layer, and rely on the self-contained output for downstream distribution. See security.md for the output-safety posture and a hardening checklist.