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: .sln or .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:

  1. Solution file named by the SOLUTION_FILE env var.
  2. Solution file named in project.toml under [com.heroku.buildpacks.dotnet].
  3. The single solution file at the root, if there's exactly one.
  4. The single project file at the root, if there's exactly one and no solution file is present.
  5. 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-x64 or --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:

project.toml
[_]
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 to Release.
  • MSBUILD_VERBOSITY_LEVEL: MSBuild verbosity; defaults to minimal.

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:

Program.cs
#: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.json is 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.Sdk with OutputType=Exe
  • Microsoft.NET.Sdk.Worker
  • Microsoft.NET.Sdk.Web
  • Microsoft.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.