# .NET apps

> Install the .NET 8, 9 or 10 SDK, deploy ASP.NET Core and other C# web apps with dotnet publish, bind Kestrel to $PORT through ASPNETCORE_URLS and fix common errors.

Source: https://zopanel.net/docs/dotnet  
Updated: 2026-10-09

ZoPanel builds .NET web applications with `dotnet publish`, runs the published app as a service of the hosting account and puts nginx with SSL in front of it. Use it for ASP.NET Core (MVC, Razor Pages, Blazor Server, minimal APIs) and any other .NET app that serves HTTP.

## Install the .NET SDK

An administrator installs it once per server:

1. Open **Runtimes**.
2. On the **.NET SDK** card, click the version to install (**.NET 10.0**, **.NET 9.0** or **.NET 8.0**). The task log shows the progress. Install every version your apps target: they live side by side.

ZoPanel offers the SDK versions Microsoft supports: .NET 10 (LTS, supported until November 2028), and .NET 9 and .NET 8, both supported until 10 November 2026. Each package (`dotnet-sdk-10.0`, …) also contains the matching ASP.NET Core runtime:

| System | Where the SDK comes from | Architectures |
| --- | --- | --- |
| Ubuntu 22.04 | 8.0 from Ubuntu's packages; 9.0 and 10.0 from Canonical's .NET backports repository (`ppa:dotnet/backports`), added automatically | amd64 and arm64 |
| Ubuntu 24.04 | 8.0 and 10.0 from Ubuntu's packages; 9.0 from the .NET backports repository | amd64 and arm64 |
| Debian 12, 13 | Microsoft's package repository, added automatically | amd64: 8.0, 9.0, 10.0. arm64: 10.0 only |

**Important:** Microsoft publishes the .NET 8 and .NET 9 packages for Debian only for amd64. On a Debian arm64 server the card offers only .NET 10 and explains why; for an app that targets `net8.0` or `net9.0` there, retarget it to `net10.0`, use an Ubuntu server, or build and run it as a container (see [Deploy with Docker](/docs/docker-deploy)). If Microsoft's repository has no configuration for the Debian release, the install stops with ".NET is not available for Debian … yet: … (deploy with a Dockerfile instead)".

### Which SDK builds the app

The app's **Version** selects the SDK. Auto-detect sets it from the project's `<TargetFramework>` (`net9.0` → 9.0; with `<TargetFrameworks>`, the newest one). During the build ZoPanel adds a `global.json` to the release that pins that SDK version, so the build log shows "Using the .NET 9.0 SDK". If the repository has its own `global.json` (in the project folder or above it), that file decides and ZoPanel does not add one. With no version, the newest installed SDK is used.

If the SDK for the version is not installed, the deployment stops with "the .NET 9.0 SDK is not installed (installed: …)". Install it under **Runtimes**, or change the project's target framework.

## What ZoPanel detects

A project is treated as .NET when its root directory contains a `.csproj` file, a `.sln` file, or a `.csproj` one folder below. ZoPanel then looks for projects up to two folders deep (for example `src/MyApp/MyApp.csproj`), skipping `bin`, `obj` and hidden folders:

- A web project (`Microsoft.NET.Sdk.Web`) is preferred and labelled **ASP.NET Core**. Otherwise the first project with `Microsoft.NET.Sdk` is used, labelled **.NET**.
- If there are several projects, the build log notes which one is published.

For a project `MyApp.csproj`, the proposed commands are:

| Field | Command |
| --- | --- |
| **Install command** | `dotnet restore 'MyApp.csproj'` |
| **Build command** | `dotnet publish 'MyApp.csproj' -c Release -o out --no-restore` |
| **Start command** | `ASPNETCORE_URLS=http://127.0.0.1:$PORT dotnet out/'MyApp.dll'` |

Detection runs in a fixed order: a `package.json`, `composer.json`, `Gemfile` or Java build file in the same directory is matched before .NET. If the wrong framework is detected, set **Root directory** to the project folder or turn off **Auto-detect** and set the commands yourself. A `Procfile` with a `web:` line replaces the start command.

## Deploy a .NET app

1. In **Websites → New website**, choose **Git deploy**, and enter the domain and the repository. See [Git deploy](/docs/git-deploy).
2. Open the website's **Deploy** tab and check **Framework** and the commands under **Build & run**.
3. To change a command, turn off **Auto-detect**, edit it and click **Save & redeploy**.
4. Add your settings under **Environment variables**.
5. Click **Deploy now**.

The new release goes live only after it answers on the **Health check path** (default `/`) with a status below 500 within 90 seconds. Until then the previous release keeps running. To build with another SDK, turn off **Auto-detect** and pick the **Version** (see [Which SDK builds the app](#which-sdk-builds-the-app)).

To deploy without Git, create the website with **Node / Proxy**, upload the source to `domains/<domain>/source` and choose **Uploaded files (File Manager)** as the source. Upload the source code, not a `publish` folder: ZoPanel runs the restore and publish itself.

## Port and URL

Kestrel must listen on `127.0.0.1` at the port in `PORT`. The proposed start command does this with `ASPNETCORE_URLS=http://127.0.0.1:$PORT`. Keep that prefix if you edit the command, and do not hard-code URLs elsewhere:

- Remove `UseUrls(...)` calls and `Kestrel:Endpoints` entries in `appsettings.json`: they override `ASPNETCORE_URLS`.
- Do not add an HTTPS endpoint to Kestrel. nginx terminates SSL and talks to the app over plain HTTP.

`PORT` changes between the two blue/green slots (the second slot uses the base port plus 10000), so never write the number into your configuration.

### Behind nginx

nginx passes `X-Forwarded-For` and `X-Forwarded-Proto`. To make the app see the visitor's IP and `https` (for redirects, cookies and OAuth callbacks), enable the forwarded headers middleware early in `Program.cs`:

```csharp
using Microsoft.AspNetCore.HttpOverrides;

var app = builder.Build();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
```

The proxy is on `127.0.0.1`, which ASP.NET Core trusts by default.

### Assembly name

The start command assumes the published DLL is named after the project file. If the project sets `<AssemblyName>`, change the start command to `out/<AssemblyName>.dll`.

## Environment variables

Variables under **Environment variables** are available during the build and at runtime. ZoPanel also sets `PORT` and `HOST=127.0.0.1`.

- Names use letters, digits and `_`, start with a letter or `_`, and have at most 64 characters. Values are one line, up to 8,192 characters. Up to 200 variables.
- Values are kept in a root-only file and passed by systemd, never on the command line.

ASP.NET Core reads environment variables as configuration. Use a double underscore for nested keys:

| Variable | Configuration key |
| --- | --- |
| `ConnectionStrings__Default` | `ConnectionStrings:Default` |
| `Smtp__Host` | `Smtp:Host` |
| `ASPNETCORE_ENVIRONMENT` | The environment name (`Production` when not set) |

For a MariaDB or PostgreSQL database created in ZoPanel, use `127.0.0.1` as the server. See [Databases](/docs/databases).

## Memory

The app, its build and everything else the account runs share the package's **RAM (MB)**. A process over the limit is stopped by the kernel and ZoPanel restarts the app. During a deployment, the build and a short overlap of the old and new release need memory at the same time.

ASP.NET Core projects use server garbage collection by default, which keeps more memory per CPU core. On small packages, switch to workstation GC or cap the heap with environment variables (heap values are hexadecimal):

```text
DOTNET_gcServer=0
DOTNET_GCHeapHardLimit=0x20000000
```

`0x20000000` is 512 MiB. See [Packages and limits](/docs/packages-limits).

## Files and logs

- The app can write only inside the account's home directory and its private `/tmp`. Keep uploads in a **Persistent paths** entry (for example `wwwroot/uploads`) or in the database: each release is a new folder, and the last 5 are kept for **Rollback**.
- **Application output** on the **Deploy** tab shows the last 400 lines of the app's console output. Turn on **Live** to follow it.
- The restore and publish output of each deployment is in **Tasks**. nginx logs are on the **Logs** tab.

## Troubleshooting

| Message or symptom | What to do |
| --- | --- |
| "the .NET SDK is not installed — install it under Runtimes" | No SDK on the server. Ask the administrator to install one on the **.NET SDK** card under **Runtimes**. |
| "the .NET 9.0 SDK is not installed (installed: …)" | The project targets a version whose SDK is missing. Install it under **Runtimes**, or change the target framework. |
| ".NET 8.0 cannot be installed: Microsoft publishes .NET 8 and 9 packages for Debian on amd64 only…" | Debian on arm64. Use .NET 10, an Ubuntu server or a container. |
| ".NET is not available for Debian … yet" when installing | Microsoft has no repository for this Debian release. Use Ubuntu or deploy with a Dockerfile. |
| `NETSDK1045: The current .NET SDK does not support targeting .NET 10.0` | The **Version** was set by hand to an older SDK than the target framework, or the repository's `global.json` pins an older SDK. Turn **Auto-detect** back on, or fix `global.json`. |
| `A compatible .NET SDK was not found` | The repository's `global.json` asks for an SDK version that is not installed. Install that version, or relax `rollForward` in `global.json`. |
| "The app did not respond on port … within 90s (it must listen on $PORT)" | Kestrel listens elsewhere (often `localhost:5000`, or a URL from `appsettings.json` or `UseUrls`). Keep `ASPNETCORE_URLS=http://127.0.0.1:$PORT` and remove other endpoint settings. |
| "The app exited during startup" | Read **Application output**. `The application to execute does not exist` means the DLL name in the start command is wrong (see Assembly name). |
| Redirects or generated links use `http://`, or every visitor IP is `127.0.0.1` | Add the forwarded headers middleware shown above. |
| The app restarts with no error | The account reached its RAM limit. Set `DOTNET_gcServer=0` or `DOTNET_GCHeapHardLimit`, or raise the package's **RAM (MB)**. |

## Related

- [Node.js and Python apps](/docs/apps-node-python): deployment pipeline, ports and blue/green releases
- [Git deploy](/docs/git-deploy)
- [Deploy with Docker](/docs/docker-deploy)
- [Packages and limits](/docs/packages-limits)
- [Host ASP.NET Core on Linux with Nginx (Microsoft)](https://learn.microsoft.com/aspnet/core/host-and-deploy/linux-nginx)
- [Configure ASP.NET Core to work with proxy servers (Microsoft)](https://learn.microsoft.com/aspnet/core/host-and-deploy/proxy-load-balancer)
