Read time: 13 minutes
The .NET Saturday is brought to you by:
Building a full-stack application is not the hard part anymore. Most devs can spin up a React frontend, a .NET API, and a PostgreSQL database without too much drama.
The painful part is deploying that stack reliably in the cloud, wiring up all the application components without drowning in connection strings, environment variables, and weird configuration errors.
That is exactly where Aspire shines. It gives you a single, understandable application model that can provision and connect your backend, frontend, database, and identity provider for both local dev and Azure.
Today, we will take a complete .NET + React + Postgres + Keycloak application, and deploy the whole thing to Azure using Aspire 13.
Let’s dive in.
The full-stack application
Here’s a quick picture of the full-stack application we will deploy to the cloud:

In essence, we have a React application that is supported by a .NET backend, which in turn stores all the data in a PostgreSQL database.
We also use Keycloak as the identity provider, which allows users to log in to the app from the browser via OpenID Connect (OIDC) and is also used by the backend to validate JWTs sent by the frontend.
Now, let’s prepare our application for deployment, starting with the database.
Adding the PostgreSQL database
To make our lives easier, we’ll define our full-stack application as an Aspire application. If you are new to Aspire, I have a beginner’s guide here.
After adding the AppHost project and installing the Azure PostgreSQL hosting integration (Aspire.Hosting.Azure.PostgreSQL NuGet package), here’s how we define our database:
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddAzurePostgresFlexibleServer("postgres")
.RunAsContainer(postgres =>
{
postgres.WithDataVolume();
postgres.WithPgAdmin(pgAdmin =>
{
pgAdmin.WithHostPort(5050);
});
});
var templateAppDb = postgres.AddDatabase("TemplateAppDB", "TemplateApp");
builder.Build().Run();
To unwrap that:
- We add a PostgreSQL resource that will deploy as an Azure Postgres Flexible Server resource
- We enable running it as a container during local dev
- We also add a PgAdmin container to make it easier to work with it locally
- We add the TemplateApp database to be used by our .NET API
Next, let’s add our identity provider.
Adding Keycloak
I covered how to deploy Keycloak to Azure over here, so today I’ll only do a quick recap.
Here’s how you add Keycloak to your Aspire application model:
var keycloakPassword = builder.AddParameter(
"KeycloakPassword",
secret: true,
value: "admin");
int? keycloakPort = builder.ExecutionContext.IsRunMode ? 8080 : null;
var keycloak = builder.AddKeycloak(
"keycloak",
adminPassword: keycloakPassword,
port: keycloakPort)
.WithLifetime(ContainerLifetime.Persistent);
if (builder.ExecutionContext.IsRunMode)
{
keycloak.WithDataVolume()
.WithRealmImport("./realms");
}
if (builder.ExecutionContext.IsPublishMode)
{
var postgresUser = builder.AddParameter("PostgresUser", value: "postgres");
var postgresPassword = builder.AddParameter("PostgresPassword", secret: true);
postgres.WithPasswordAuthentication(postgresUser, postgresPassword);
var keycloakDb = postgres.AddDatabase("keycloakDB", "keycloak");
var keycloakDbUrl = ReferenceExpression.Create(
$"jdbc:postgresql://{postgres.Resource.HostName}/{keycloakDb.Resource.DatabaseName}"
);
keycloak.WithEnvironment("KC_HTTP_ENABLED", "true")
.WithEnvironment("KC_PROXY_HEADERS", "xforwarded")
.WithEnvironment("KC_HOSTNAME_STRICT", "false")
.WithEnvironment("KC_DB", "postgres")
.WithEnvironment("KC_DB_URL", keycloakDbUrl)
.WithEnvironment("KC_DB_USERNAME", postgresUser)
.WithEnvironment("KC_DB_PASSWORD", postgresPassword)
.WithEndpoint("http", e =>
{
e.IsExternal = true;
e.UriScheme = "https";
});
}
As you can see, to run it locally (IsRunMode), all we do is add a data volume (to persist changes) and import a realm from our realms folder (if available).
But to run it in the cloud, we have to do a few extra things to allow Keycloak to keep the realm data in our Azure PostgreSQL DB:
- Prepare credentials that Keycloak can use to connect to the database
- Define a new DB for Keycloak
- Set the KC_HTTP_ENABLED, KC_PROXY_HEADERS, and KC_HOSTNAME_STRICT environment variables so Keycloak can run properly as an Azure Container App.
- Set all the environment variables needed for Keycloak to connect to the DB
- Make Keycloak’s endpoint external, so we can access it from our browser to configure it.
Again, I explained all that in detail in my previous post.
Next, let’s add our .NET backend.
Adding the .NET Backend
This is the easiest part, since Aspire includes excellent support for all things .NET.
Here’s the code:
var keycloakAuthority = ReferenceExpression.Create(
$"{keycloak.GetEndpoint("http").Property(EndpointProperty.Url)}/realms/templateapp"
);
var api = builder.AddProject<TemplateApp_Api>("api")
.WithReference(templateAppDb)
.WaitFor(templateAppDb)
.WithEnvironment("Auth__Authority", keycloakAuthority)
.WaitFor(keycloak)
.WithExternalHttpEndpoints()
.WithHttpHealthCheck("/health/ready");
The WithReference call shares the database connection info with our API (locally and in the cloud), and WaitFor is used to ensure the DB is ready before the API starts.
To get our API to talk to Keycloak (for JWT validation), we need to provide it with the Authority, which is the URL to the Keycloak server, including the realm configured for your application.
Using a ReferenceExpression lets Aspire resolve the final URL once Keycloak has been deployed to Azure and then hand it over to our API as an environment variable.
Also, the assumption here is that there will be a realm named templateapp there, which, for now, we’ll have to create or import manually once the deployment is complete.
Next, the tricky part: the frontend.
Adding the React frontend
I previously covered how to add a React app to Aspire here, but support for frontend frameworks, particularly for JavaScript-based ones, received a nice refresh in Aspire 13.
Since we are using Vite to build and run our React app, we can add it to our application model by installing the new JavaScript hosting integration (Aspire.Hosting.JavaScript NuGet package) and adding this code:
var frontend = builder.AddViteApp("frontend", "../TemplateApp.React")
.WithReference(api)
.WaitFor(api)
.WithEndpoint(endpointName: "http", endpoint =>
{
endpoint.Port = builder.ExecutionContext.IsRunMode ?
5173 : null;
});
The port specification there is just my personal preference, since I like to have my React app loaded in my browser with a stable port that I can refresh any time.
However, the challenge comes with trying to deploy the Vite app to Azure. Aspire can turn it easily into a container and deploy it as an Azure Container App, but here’s the challenge:
How do you provide the URLs of our .NET backend and of our Keycloak server to a Single Page Application (SPA) that runs entirely in the browser?
You don’t know those URLs until Aspire completes provisioning the .NET API and Keycloak as Container Apps, and you can’t set environment variables on something that just runs in the browser.
But as with everything else, there’s a way.
Adding a YARP web server
There are a few ways to solve the Vite app cloud hosting problem, including the popular Backend For Frontend (BFF) pattern, but a simpler way is to add a YARP web server to the mix.

Here’s what we get by introducing YARP:
- We can deploy our Vite/React app as static files inside the YARP server
- Any time the React app sends an outbound request to a location like “/api”, YARP can catch it and redirect to the actual location of the .NET backend
To enable this, start by installing the YARP hosting integration (Aspire.Hosting.Yarp NuGet package) and then add this code:
if (builder.ExecutionContext.IsPublishMode)
{
builder.AddYarp("frontend-server")
.WithConfiguration(c =>
{
// Always proxy /api requests to backend
c.AddRoute("api/{**catch-all}", api)
.WithTransformPathRemovePrefix("/api");
})
.WithExternalHttpEndpoints()
.PublishWithStaticFiles(frontend);
}
As you can see, we only want to do this in Publish mode. During local development, YARP is not needed since Vite can take care of everything.
Notice how we enable an external endpoint on YARP, so we can reach the frontend from our browser and call PublishWithStaticFiles so that all the React frontend files get copied into the YARP container.
One more thing you would need to do, this time in your .NET API directly, is to expose an endpoint that can return the full Keycloak Authority URL:
public static class GetConfigurationEndpoint
{
public static void MapGetConfiguration(this IEndpointRouteBuilder app)
{
// GET /config
app.MapGet("/", (IOptions<AuthOptions> authOptions) =>
{
return Results.Json(new ConfigurationDto(authOptions.Value.Authority));
})
.Produces<ConfigurationDto>();
}
}
The frontend will call this endpoint the same way it makes any other backend call, and it will return the full location of Keycloak so it can start the OIDC authentication process.
Now, let’s try it out.
Deploying the full-stack application
Before kicking off the deployment, you can confirm everything runs locally with an aspire run call, which will take you to a local dashboard like this:

Notice how both the Keycloak database and the YARP server are not present in the dashboard, since we only included them in Publish mode.
To deploy the application, we can use either the Azure Developer CLI (azd up) or the newer aspire deploy command of the Aspire CLI.
Either way, after a few minutes, you’ll end up with something like this in your Azure subscription:

You can see our .NET API, YARP frontend, and Keycloak server there, along with our PostgreSQL database (and a bunch of other supporting infra we didn’t have to even think about).
From there, you should head to your Keycloak server to configure your templateapp realm:

And then you can browse to your frontend-server, which corresponds to our YARP-hosted React application:

Click on Login to authenticate via Keycloak:

And then start making changes as an authenticated user:

Mission accomplished!
Wrapping Up
Everybody can build full-stack applications these days. Most devs stall when it is time to wire everything up in Azure without breaking things.
Aspire changes that. It lets you describe your full stack once, keep the connections and config in a single place, and reuse the same model for local dev and cloud deployments.
Instead of fighting connection strings and ad hoc scripts, you focus on the parts that actually move your product forward.
And that’s it for today.
See you next Saturday.
Whenever you’re ready, there are 3 ways I can help you:
-
.NET Backend Developer Bootcamp: A complete path from ASP.NET Core fundamentals to building, containerizing, and deploying production-ready, cloud-native apps on Azure.
-
Building Microservices With .NET: Transform the way you build .NET systems at scale.
-
Get the full source code: Download the working project from this newsletter, grab exclusive course discounts, and join a private .NET community.