How to Deploy an ASP.NET Core App (.NET 8 or .NET 10)
An ASP.NET Core app deploys to Chajio Cloud as dotnet new made it: no Dockerfile, no start command and no port code. Push it to GitHub and it is built with the .NET SDK your project targets, then runs on Linux as two copies with HTTPS. This guide covers the few things worth adding: a health check for APIs, HTTPS awareness, a PostgreSQL database, and the shared keys that two copies of an app need.
On this page
Before you start
- A Chajio Cloud App Hosting plan. The Free plan is enough to follow along. Once you order, you get an email with your sign-in details, and the first time you sign in you choose your own password.
- Your code on GitHub, in a repository you own or can access. A public repository you don't own works too.
- An ASP.NET Core app on .NET 8 or later, such as one made with
dotnet new webapp, with its.csprojcommitted. Rundotnet new gitignoreonce so thebinandobjfolders stay out of Git.
Step 1: Get your app ready
For a Razor Pages or MVC app there is nothing to change. When the platform finds a .csproj, it reads the project’s TargetFramework, installs the matching .NET SDK, publishes your app in Release mode for Linux and starts it on the right port. The build log shows each step:
- SDK version detection
- Detected .NET project: /workspace/MyApp.csproj
- Detected version requirement: ^10.0
- Resolved .NET SDK version 10.0.402 (linux-amd64)
- Publish app
- Running dotnet publish /workspace/MyApp.csproj --runtime linux-x64 ...
- Process types
- Found web: bash -c cd bin/publish; ./MyApp --urls http://*:$PORTYour app starts with --urls http://*:$PORT and logs Now listening on: http://[::]:3000, so code that reads PORT and calls UseUrls isn’t needed. It does no harm if you already have it.
Choose your .NET version
The SDK follows your project file: net10.0 builds with the newest .NET 10 SDK, and net8.0 with the newest .NET 8 SDK.
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>A global.json at the root of your repository is read first, and the build log says “Detecting version requirement from root global.json file”. It follows its own rollForward rule, so this one builds with the newest 8.0 SDK, not exactly 8.0.100:
{
"sdk": {
"version": "8.0.100",
"rollForward": "latestFeature"
}
}Web APIs need a health check route
Before a new version takes visitors, the platform checks that it answers its Health check path, / unless you change it. Razor Pages and MVC apps answer /. The dotnet new webapi template only maps /weatherforecast, and maps its OpenAPI document only in Development, so / returns 404 and the deploy never becomes ready. Add a health check:
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/health");You’ll point the health check at /health in Step 4.
HTTPS and your visitors’ addresses
HTTPS is handled before requests reach your app, so the app itself sees plain HTTP: Request.Scheme is http, and the client IP is the proxy’s. One environment variable, which you’ll add in Step 4, fixes both:
ASPNETCORE_FORWARDEDHEADERS_ENABLED=trueWith it, sign-in redirects, absolute links and secure cookies use https, and you see each visitor’s real IP address. UseHttpsRedirection can stay: it logs “Failed to determine the https port for redirect” at startup and then does nothing, because visitors who type http:// are already sent to HTTPS.
Step 2: Create a project
A project is the home for one app, along with its database and domain. In your dashboard, open Projects and click New project:
- Name
- Lowercase letters, numbers and hyphens, up to 24 characters. It becomes part of your app’s free web address and can’t be changed later, so pick something short, like shop or api.
- Plan
- The plan the project draws its resources from.
- Resources
- CPU, memory and storage for this project, pre-filled with what your plan has left. The defaults suit most apps.
Click Create project. Setting it up takes about a minute, and you land on the deploy guide straight away, which walks you through three short steps: Source, Configure and Deploy. You can pick your repository while the project finishes setting up.
Step 3: Connect your repository
On the Source step, choose where your code comes from:
- Your repository
- Click Connect GitHub and approve access, which lets Chajio Cloud read your code and hear about your pushes. Then choose the repository and the branch to deploy, usually main. From now on, every push to that branch deploys automatically.
- Public repository
- Paste the URL of any public GitHub repository, no GitHub sign-in needed. Since you don’t own it, pushes can’t trigger deploys: deploy by hand, or tick Auto-deploy new releases to follow the project’s published releases.
If your app lives in a subfolder of the repository, as in a monorepo, set Build context to that folder, for example apps/web. Otherwise leave it as ., the repository root. Then click Connect repository and Continue.
Step 4: Configure
The Configure step has two runtime settings, both changeable later without a rebuild, and your environment variables.
- Container port
- Leave it at 3000. Your app is started on whatever port is set here.
- Health check path
/for Razor Pages and MVC apps,/healthfor a Web API with the health check above. Any path that answers quickly without a login works.
Add the variable that makes your app aware of HTTPS:
ASPNETCORE_FORWARDEDHEADERS_ENABLED=trueEvery variable becomes part of your app’s configuration, so you read it through IConfiguration like a value in appsettings.json. A double underscore stands for the colon in a key: Smtp__Host is builder.Configuration["Smtp:Host"]. Keep secrets here rather than in appsettings.json.
Variables live on the project's Environment tab. Add each one as a name and a value, then click Save & restart: your app restarts with the new values in moments, no rebuild needed.
Your build receives the variables too. A Dockerfile gets the ones it declares with an ARG line, and an auto-detected build gets all of them. A variable that went into your current build is marked Build. Change one of those and the dashboard offers to Redeploy, because only a new build picks up the new value.
PORT is set by the platform from the container port, so you can't set it yourself. If you attach a database, its connection variable appears here too, locked, because the platform manages it for you.
Click Save and continue.
Step 5: Deploy
The Deploy step shows a summary of the repository, branch and build context. Click Deploy now and the build log starts streaming: your code is fetched, dependencies are installed and your app is built and started. The deployment moves through Queued, Building and Deploying to Success.
The first build takes a few minutes. Later builds reuse the parts that didn't change, so they are usually quicker. When it finishes, your app is live on a free live.chajio.cloud address with HTTPS already switched on. You'll find the address on the project's Overview.
If the deployment fails, the dashboard names the stage that broke and why, and the build log shows the details. Our troubleshooting guide covers the usual causes.
In our tests a first .NET build took about five minutes, most of it installing the SDK and restoring packages. The app was ready a second or two after it started, using about 36 MB of memory.
Adding a PostgreSQL database
Create a PostgreSQL database on the project’s Databases tab and give it the variable name DATABASE_URL; How to Add a Database to Your App walks through it. The database reaches your app as an address like postgresql://user:password@host:5432/db. Npgsql doesn’t accept that format, and passing it as it is stops your app at startup with:
ArgumentException: Format of the initialization string does not conform to specification starting at index 0.Add the packages, with the data protection package at a version that matches your .NET:
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.AspNetCore.DataProtection.EntityFrameworkCore --version 10.0.15Without --version, the second package can resolve to a release built for a newer .NET and refuse to install on a net10.0 project, saying it is “incompatible with ‘all’ frameworks”. On .NET 8, choose 8.0 releases of both packages.
Then convert the address once, in Program.cs. This version also keeps your app’s data protection keys in the database and adds the health check, both explained below:
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Npgsql;
var builder = WebApplication.CreateBuilder(args);
// DATABASE_URL is a postgresql:// address; Npgsql wants Host=…;Database=… pairs.
static string ToNpgsql(string url)
{
var uri = new Uri(url);
var user = uri.UserInfo.Split(':', 2);
return new NpgsqlConnectionStringBuilder
{
Host = uri.Host,
Port = uri.Port > 0 ? uri.Port : 5432,
Database = uri.AbsolutePath.TrimStart('/'),
Username = Uri.UnescapeDataString(user[0]),
Password = user.Length > 1 ? Uri.UnescapeDataString(user[1]) : null,
}.ConnectionString;
}
var databaseUrl = Environment.GetEnvironmentVariable("DATABASE_URL")!;
builder.Services.AddDbContext<AppDbContext>(o => o.UseNpgsql(ToNpgsql(databaseUrl)));
// One set of keys for every copy of the app, so forms and sign-ins work on all of them.
builder.Services.AddDataProtection().PersistKeysToDbContext<AppDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapRazorPages();
app.MapHealthChecks("/health");
app.Run();
public class AppDbContext(DbContextOptions<AppDbContext> options)
: DbContext(options), IDataProtectionKeyContext
{
public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();
}If you’d rather not convert it, you can instead set a connection string yourself, such as ConnectionStrings__Default=Host=…;Database=…;Username=…;Password=… with the credentials from the database’s card, and read it with GetConnectionString("Default").
Two copies of your app
Every plan runs two copies of your app, and that changes two things for ASP.NET Core.
Share the data protection keys
ASP.NET Core signs antiforgery tokens and sign-in cookies with data protection keys, and each copy makes its own unless told otherwise. A form rendered by one copy and posted to the other then fails with 400 Bad Request, seemingly at random, and visitors are signed out. The startup warning Storing keys in a directory … that may not be persisted is the clue. The PersistKeysToDbContext line above keeps one set of keys in the database for both copies; in our tests, the same form then posted successfully to either copy.
Create tables with migrations
On the first deploy both copies start together. With Database.EnsureCreated() in Program.cs, both try to create the same tables on the empty database, and one crashes with 42P07: relation "DataProtectionKeys" already exists. Use EF Core migrations instead, including one for the keys table:
dotnet ef migrations add InitialCreateApply them as your app starts, with Database.Migrate(), or from a migration bundle. If the first deploy’s logs show one copy failing a migration, it restarts and finds the work done; the database guide explains why that recovers on its own.
Connect your domain
Your app already works on its free address. To serve it from your own domain, open the project's Domains tab:
- Enter your domain, for example
www.example.com, and click Add domain. - The dashboard shows two DNS records to add where your domain's DNS is managed. A TXT record proves the domain is yours. A CNAME record routes visitors to your app, or an A or ALIAS record for a root domain like
example.com. Copy the values exactly as shown. - Click Verify. Once both records check out, a free HTTPS certificate is issued automatically, usually in under a minute, and your app is live on your domain.
DNS changes can take a while to spread, so if Verify doesn't find a record yet, give it a few minutes; the dashboard also re-checks on its own. The full walkthrough, including root domains and Cloudflare, is in How to connect your domain to your app.
After launch
- Every push deploys. Push to your branch and a new deployment starts on its own. Your current version keeps serving visitors until the new one passes its health check, so updates don't cause downtime. You can also deploy by hand from the Deployments tab.
- Roll back in seconds. If a release misbehaves, open Deployments and click Rollback to this version on an earlier successful deployment. Nothing is rebuilt; the previous version simply comes back.
- Watch it run. The Logs tab streams your app's output live, with secrets hidden. The Overview shows your app's status and address, with Redeploy, Restart and Stop buttons.
Your app runs as two copies
Every plan runs two copies of your app, so a crash or an update never takes it offline. That has one consequence worth designing for: anything an app keeps only in its own memory or on its own disk isn't shared with the other copy, and it's gone after a redeploy.
Keep data protection keys and sessions in your database, as above, and uploaded files in object storage rather than in wwwroot: each deploy starts from a fresh copy of your app.
Common problems
A Web API deploys but never becomes healthy
The health check gets a 404 at /. Add app.MapHealthChecks("/health") and set Health check path to /health.
Forms fail with 400 Bad Request, some of the time
The two copies have different data protection keys. Store them in the database with PersistKeysToDbContext, as shown above.
“Format of the initialization string does not conform to specification”
DATABASE_URL was passed to Npgsql as it is. Convert it to a connection string first.
Sign-in redirects or generated links use http://
Set ASPNETCORE_FORWARDEDHEADERS_ENABLED=true on the Environment tab.
“relation … already exists” on the first deploy
Both copies ran EnsureCreated() at once. Create your tables with EF Core migrations instead.
A package is “incompatible with ‘all’ frameworks”
That version of the package is built for a newer .NET than your TargetFramework. Add it again with a --version that matches your .NET version.
For anything else, Deployment Failed? How to Find and Fix the Cause explains how to read a failed deployment. Plans and a summary of everything here are on the .NET hosting page.
Learning path
Deploy your first app
- 1
- 2
- 3
- 4
Keep reading
How to Add a Database to Your App
Create a managed PostgreSQL, MariaDB or MongoDB database next to your app and have its connection details handed to your app automatically, with examples for Node.js, Python, Django, .NET and Rails.
How to Connect Your Domain to Your App (with Free HTTPS)
Put your app on your own domain in three DNS steps. Covers www versus root domains, what each DNS record does, Cloudflare, and what to check when a domain will not verify.
Deployment Failed? How to Find and Fix the Cause
Every failed deployment tells you which stage broke and why. How to read it, and the fixes for the failures we see most: missing start commands, port mix-ups, health checks and memory.
Why Your Website Stays Online Even When It Gets Updated
Ever seen a "We're down for maintenance" page? Here is why that happens, why it does not have to, and what good hosting looks like when updates run quietly in the background.
Host your .NET app on Chajio Cloud
Free for your first month, with a managed database and two replicas of your app on every plan.