Docs menu
.NET on Primcloud
How Primcloud detects a .NET app, resolves the build target, publishes it, caches NuGet packages, and infers process types.
On this page
The build detects a .NET app from the files in your repo, resolves which project or solution to build, runs dotnet publish to produce a runnable output, caches NuGet packages between builds, and, when you don't ship a Procfile, infers the process types from each project's SDK. This page walks through each phase and the settings that shape it.
Detection#
Primcloud runs the .NET toolchain when the root of your repo contains any of:
- A solution file:
.slnor.slnx - A project file:
.csproj,.vbproj, or.fsproj - A single C# file:
.cs
If none of the above are present, the build fails with a "no language detected" error before starting.
Build target precedence. When more than one candidate exists, the build picks a target in this order:
- Solution file named by the
SOLUTION_FILEenv var. - Solution file named in
project.tomlunder[com.heroku.buildpacks.dotnet]. - The single solution file at the root, if there's exactly one.
- The single project file at the root, if there's exactly one and no solution file is present.
- The single C# file-based app at the root, if there's exactly one and no solution or project files are present.
Ambiguous cases fail the build: multiple solution files with none explicitly named, multiple project files with no solution, or multiple .cs files without a solution or project.
Install phase#
For .NET, dependency installation and compilation happen together as part of dotnet publish. NuGet packages listed in your project's <PackageReference> entries (or in packages.config for legacy projects) are restored into a NuGet package cache before compilation begins.
Local .NET tools declared in .config/dotnet-tools.json are also restored during the install phase and made available on PATH for the rest of the build.
Compile phase#
The build runs dotnet publish against the selected target with these baseline arguments:
--runtime linux-x64or--runtime linux-arm64, matching the target architecture.-p:PublishDir=bin/publish: a consistent output location the release phase reads from.--artifacts-path <temp>: intermediate build artifacts are placed outside the final image so they don't ship in the runtime layer.
Configuring the build. Two config surfaces shape the publish command:
Via project.toml at the repo root:
[_]
schema-version = "0.2"
[com.heroku.buildpacks.dotnet]
solution_file = "MyWebApp.sln"
[com.heroku.buildpacks.dotnet.msbuild]
configuration = "Debug"
verbosity = "quiet"
Via environment variables set on your environment (Secrets panel in the dashboard):
SOLUTION_FILE: solution file to build.BUILD_CONFIGURATION: MSBuild configuration; defaults toRelease.MSBUILD_VERBOSITY_LEVEL: MSBuild verbosity; defaults tominimal.
Environment variables take precedence over project.toml.
File-based apps. A bare .cs file at the root (no project, no solution) is built as a .NET file-based app using Microsoft.NET.Sdk and TargetFramework=net10.0 by default. Override with preprocessor directives inside the file:
#:sdk Microsoft.NET.Sdk.Web
#:property TargetFramework=net11.0
#:property PublishAot=false
Cache behavior#
The .NET build writes three cached layers that Primcloud persists in a per-app cache image between builds:
- NuGet package cache: restored
<PackageReference>dependencies. Reused when the package graph hasn't changed; cleared automatically after twenty consecutive reuses. - .NET SDK: the SDK version resolved from
TargetFramework/global.jsonis cached across builds so a repeat deploy with the same SDK doesn't re-download it. - .NET runtime: the runtime that ships in the final image.
You'll see either Reusing package cache or Clearing package cache after N uses in the build log's NuGet cache section.
Release phase conventions#
Procfile takes precedence. If your repo has a Procfile at the root, Primcloud uses it verbatim to define process types, the same as every other language on Primcloud. See Pod Formations for the file format.
Auto-detected process types kick in when there's no Procfile. The build scans your project files for executable targets and generates one process type per matching project, using these SDKs:
Microsoft.NET.SdkwithOutputType=ExeMicrosoft.NET.Sdk.WorkerMicrosoft.NET.Sdk.WebMicrosoft.NET.Sdk.Razor
Process naming. A single executable project produces one web process. If your solution contains multiple executables, each gets its own process type named after the project (or its AssemblyName property), lowercased, with spaces, dots, and underscores replaced by hyphens and non-alphanumeric characters stripped.
Port binding. For web-flavored SDKs (Microsoft.NET.Sdk.Web and Microsoft.NET.Sdk.Razor), Primcloud appends --urls http://*:$PORT to the launch command, so an ASP.NET Core web app binds to the port Primcloud sets with no code of its own. Non-web processes are launched without extra arguments. A project that serves HTTP but isn't one of those SDKs (a worker service that also exposes an endpoint, a custom entrypoint) has to read $PORT itself, either directly or by setting ASPNETCORE_URLS to http://0.0.0.0:$PORT.
Runtime environment. The following variables are set on every process at runtime:
| Variable | Value | Purpose |
|---|---|---|
DOTNET_ROOT |
Path to the installed .NET runtime | So dotnet can find its shared framework. |
DOTNET_CLI_TELEMETRY_OPTOUT |
true |
Opts out of .NET CLI telemetry. |
DOTNET_NOLOGO |
true |
Mutes the .NET welcome banner. |
DOTNET_RUNNING_IN_CONTAINER |
true |
ASP.NET Core uses this to skip host-specific setup. |
PORT |
5000 |
The port your web process binds to. Fixed, and set only on web; worker and other process types never receive a PORT. |
PATH |
With .NET root prepended | So dotnet and installed tools are on PATH. |
Anything you set in the Secrets panel is layered on top of these at runtime.