frontback / runtimes / dotnet

Deploy a .NET app

Example ASP.NET Core service, deployed to a URL in Europe in a minute. What it costs to run, how to scale it out when one instance stops being enough, and what Native AOT is really worth.

Basic app example

This is what dotnet new web gives you, unchanged. There is no port to read and no host to configure: the start command binds whatever port the platform hands the process, which is the one thing that would otherwise be different here.

Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello from .NET on Frontback");

app.Run();

Run it locally first; nothing about it is Frontback specific:

$ dotnet new web -o hello && cd hello && dotnet run
  Now listening on: http://localhost:5000

Prerequisites

Two things, once per machine. The CLI is a single self-contained binary with no runtime and no daemon behind it, and the login is a device code, so it works over SSH and inside a container just as well.

$ brew install frontback/tap/frontback

$ frontback login
  open the URL it prints and enter QK7X-4M2P
 logged in as anna@acme.eu (organization acme)

The Go toolchain route works anywhere, whichever system you are on, and the binary is self-contained either way.

For CI, or anything else running unattended, use an API key instead of a device login: frontback login --with-token, or put FRONTBACK_TOKEN in the environment and store nothing at all. A key can be narrower than the person who made it.

Deploy it from your terminal

frontback deploy finds your .csproj (or .fsproj, or the solution that holds them), restores in its own cached layer, publishes with dotnet publish -c Release and ships the output on the matching runtime image. No Dockerfile, no YAML; open the URL it prints and your service is answering.

$ frontback deploy

  created project hello-dotnet
  service   hello-dotnet  production
> restoring hello.csproj (nuget cache)
> publishing -c Release
 built (23.4s)
 base mcr.microsoft.com/dotnet/aspnet:10.0.10-noble
 v1 released to production
 live (1.1s)
  https://hello-dotnet.run.frontback.eu

Only the project files are copied before the restore, which is what makes that layer cacheable: a warm restore is seconds where a cold one is minutes. Every project file in the tree goes in, not just the entry one, so a ProjectReference to a sibling resolves. Paket repositories are handled the same way.

Or deploy on git push

Connect the repository (GitHub App, Gitea, or a deploy key), bind it to your project and track a branch per environment. A push deploys only the services the commit changed, grouped into one release: staging first, production one promote away, rollback one more.

Which image your app lands on

Framework-dependent publish is the default: the output carries your application and the runtime lives in the base image. That keeps the app layer a few megabytes instead of seventy, lets every .NET service on the platform share one runtime layer, and means a runtime CVE is fixed by rebasing rather than by rebuilding your image. The choice is mechanical, and the build plan says which one it made.

A web app
aspnet:<version>-noble. The full image, because binding the port the platform hands you needs a shell.
A console or worker
runtime:<version>-noble-chiseled-extra. Distroless, non-root, half the size, with ICU and time zone data kept.
Self-contained or Native AOT
runtime-deps:<version>-noble-chiseled-extra, with the -aot variant for AOT output. No .NET in the image at all, because your binary brought its own.
Blazor WebAssembly
No runtime image and no worker. A standalone Blazor project publishes a wwwroot that runs in the browser, so it is deployed as a static site with the SPA fallback on.

Configuration, when you need it

The deploy above needed zero config. When you outgrow the defaults, frontback.json and your own project file are the whole surface:

"dotnet": "10"
Pins the major. The ladder is frontback.json, then the target framework (net10.0), then global.json, then .tool-versions, then the default. The target framework wins over global.json on purpose: it states which shared framework the output runs on, so it decides the runtime image, while global.json only decides which SDK compiles the tree.
<PublishAot>true</PublishAot>
Turns the publish into an ahead-of-time native compilation. Read the trade-off below before you switch it on; it is opt in for a reason.
<InvariantGlobalization>true</InvariantGlobalization>
Drops ICU and takes the smaller chiseled image. Only do this if you meant it: culture-aware formatting throws at run time afterwards, which is why the image that keeps ICU is the default.
"build" + "start"
Take over entirely. Everything your build writes to dist/ lands in the image, and start is shell form, so $PORT interpolates.

A solution holding one runnable project resolves to it. Several runnable projects with one web project among them resolve to the web one, and the plan says so. Anything less clear cut is an error that names every candidate, because guessing which of two services to deploy is worse than asking. frontback pack explain prints the whole plan, image tags included, before anything runs.

When one instance is not enough

Make the machine bigger, or run more copies of it. Both are settings on the service in the console, and both take effect through an ordinary release, so staging sees the change before production does and a rollback is one click.

A bigger machine
Shared 256 MB through Shared 2 GB share cores with neighbours and burst into whatever is idle; the Performance sizes pin cores to your microVM instead. A resize is a restart: it mints a release like any deploy, so it promotes and rolls back the same way.
More replicas
A replica is a warm sandbox kept ready for the service, and the floor is never reaped when traffic stops. 1 on Free, 5 on Pro, 20 on Business, 100 on Enterprise.
Always on
Turn off idle stop when a cold start is not acceptable, from Pro up. Everything below it sleeps when nobody is asking and costs nothing while it sleeps.

A framework-dependent .NET service wants more headroom per instance than a compiled binary does, so the machine size is usually the first dial you reach for. Native AOT output moves that answer back down a size, which is the other reason to consider it.

What it costs

  • Free, €0Runs your service on demand: it stops when idle and costs nothing asleep.
  • €4 / €6 a monthNative AOT output fits Shared 256 MB; a framework-dependent service is comfortable on Shared 512 MB.
  • Pro, €19 a month Always-on and every shared machine size start on Pro.

Where .NET shines here, and where it does not

  • Native AOT starts in tens of milliseconds instead of hundreds, which on a platform that scales to zero is the single largest cold-start win a .NET application can have.
  • Framework-dependent publish keeps your layer small: the runtime is in the base image, shared by every .NET service, and patched by rebasing rather than rebuilding.
  • Chiseled base images are half the size of the full ones, carry no shell or package manager, and run as a non-root user without you configuring anything.
  • Without AOT the JIT warms up for hundreds of milliseconds, so a cold start is something your first visitor notices.
  • AOT trims code no call site names statically, so reflection-based serializers, ORMs and assembly-scanning DI compile cleanly and then throw on the first request. Run the published binary before you deploy it. In F#, printf-style formatting and quotations are reflection too.
  • Idle memory sits above what Go or Rust need, so the smallest machine is tight for anything framework-dependent.

Questions .NET developers actually ask

No. <code>frontback deploy</code> claims the tree from a <code>.csproj</code>, <code>.fsproj</code>, <code>.vbproj</code> or a solution, restores, publishes and picks the runtime image itself. If you commit your own Dockerfile it wins, because a Dockerfile is an instruction rather than evidence.

.NET 10 by default, the current LTS, on runtime 10.0.10 built by SDK 10.0.302. .NET 9 and 8 are still offered while they are supported. Older majors are deliberately absent: a tree targeting them gets an error listing what is offered, because serving a runtime nobody patches is worse than serving none.

You do not have to answer that. The start command binds the port the platform hands the process, which is also why a web app gets the runtime image that has a shell. Setting <code>ASPNETCORE_URLS</code> yourself still works if you want the control.

If your app avoids runtime reflection, yes: it is the biggest cold start improvement available to a .NET service here. Publish it locally and run the produced binary first, and let the compiler help with <code>&lt;IsAotCompatible&gt;true&lt;/IsAotCompatible&gt;</code>. Trimming removes what no call site names, so a reflection-heavy dependency links cleanly and fails when it runs, and nothing in a build can detect that for you.

Yes. F# projects build exactly like C# ones, including Paket repositories, with one caveat under AOT: printf-style formatting and code quotations go through reflection. A standalone Blazor WebAssembly project is deployed as a static site with the SPA fallback on, so no runtime image is built and no worker ever starts.

One runnable project is chosen and the choice is reported. With several, a single web project among them wins. Anything less clear cut is an error naming every candidate. The language does not enter into it, so a solution with both a .csproj and an .fsproj resolves the same way.

Yes. Postgres and Redis run as services inside your project, and for serverless we recommend the managed libsql database: it speaks the SQLite dialect over HTTP, so there is no connection pool to exhaust and nothing to keep alive between requests.

A framework-dependent service is comfortable on Shared 512 MB at €6 a month always-on, and Native AOT output fits Shared 256 MB at €4. Always-on starts on the Pro plan (€19 a month); the Free plan runs services on demand and stops them when idle.

On our own data servers in the EU. EU hosting is the default on every plan, not a paid add-on, which is usually the answer your compliance team is looking for.

Deploy something else

Go beyond what seems possible.