ASP.NET Core is not supported on Heroku. Read how I didn't care about that and made it work anyways (and all the hoops involved).
Recently, I started a hobby project that I for once was determined to finish. And that within a strict timeline too: NamR, an app to register baby names and compare them with your significant other’s. (Disclaimer: it's a Norwegian app designed by a developer for personal use. You have been warned).
As a .NET developer, and a notorious non finisher of hobby projects, I wanted a simple, clean and, most importantly, free way of running my app. Almost everyone I know that don’t see themselves as a .NET developer recommends Heroku for these very reasons, so I though I'd give it a try.
However, .NET is not supported on Heroku 😢. Though there is a way to do it anyway: Containers 🐋
With various assurances that a containerized .NET app will indeed run on Heroku, I decided to try it out. But as you might have guessed, completing this task wasn't as straightforward as those assurances assured. And that's why I'm writing this blog post.
The quirks of running .NET in a container on Heroku
NamR’s architecture is as simple as:
- A .NET 5 Web Application with a Blazor Web Assembly front-end running on a free Dyno.
- A Postgres database using the Heroku Postgres add-on.
Since this blog post is all about how I got the app running, I will not go into more details about the app itself, nor how to set things up in Heroku (they have plenty of documentation for that purpose).
The rest of this post will refer to (and show) examples from NamR's code. Feel free to have a look.
Before deploying I wanted to have a functioning application that talked to the database running locally. After that, I went ahead and created my Dockerfile and pushed the image to Heroku so that I could start working through the hoops necessary to make it run.
The port problem
The first quirk is how Heroku dynos handle ports when running containers. Normally when you run a container you specify port mappings and such in the Dockerfile. In Heroku, your container needs to support being assigned a random port through the $PORT
variable. For a .NET app, that means you need this in your Dockerfile:
ENV ASPNETCORE_URLS http://*:$PORT
But wait, there’s more! You also have to configure Kestrel, .NET’s default web server, to listen on this port:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
// Important part!
var portEnv = Environment.GetEnvironmentVariable("PORT");
if (portEnv != null)
{
webBuilder.ConfigureKestrel(serverOptions =>
{
serverOptions.Listen(IPAddress.Any, Convert.ToInt32(portEnv));
});
}
webBuilder.UseStartup<Startup>();
});
}
With that out of the way, you should be able to reach your app on Heroku so that you can behold the error message it displays in all its glory. But hey! At least this proves that you have configured the port correctly. Because the error message is coming from the application itself, and its telling you that it cannot access the database.
Next problem: connecting to the Heroku Postgres database.
Heroku provides connection details for the database as an URL via an environment variable: DATABASE_URL
. But EF Core will not eat connection details in any other format than a connection string
like this one (more properties exist):
Host=localhost;Database=namr;Username=namr;Password=supersecret123
To fix that, you need to tweak your Startup to handle translating the DATABASE_URL
to a connection string:
public void ConfigureServices(IServiceCollection services)
{
var dbUrl = Configuration["DATABASE_URL"];
if (dbUrl != null)
{
// class shamelessly copied from SO: https://stackoverflow.com/questions/51602947/using-entity-framework-asp-net-mvc-core-and-postgresql-with-heroku
var builder = new PostgreSqlConnectionStringBuilder(dbUrl)
{
Pooling = true,
TrustServerCertificate = true,
SslMode = SslMode.Require
};
services.AddDbContext<ListContext>(options => options.UseNpgsql(builder.ConnectionString));
}
else
{
services.AddDbContext<ListContext>(options => options.UseNpgsql(Configuration.GetConnectionString("Default")));
}
// more stuff
}
Migrate the database
Another issue is how to handle migrations. Personally, I prefer to run migrations locally using a command line tool like dotnet ef
, and in various environments this should be done during deployment. However, in this case I decided to just run migrations during application startup:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyDbContext context)
{
context.Database.Migrate();
// more stuff...
}
What about HTTPS?
You may now visit your app. Open a browser and type in "yourappname.herokuapp.com" and witness the beautiful 404 you're greeted with. That’s right. There’s one more thing: HTTPS redirect.
This is usually something you get out of the box with the code provided in a .NET Web App template, but in a container on Heroku you need to handle this a little different (this may also apply behind other forms of reverse proxies):
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (!env.IsDevelopment())
{
var forwardedHeadersOptions = new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
};
forwardedHeadersOptions.KnownNetworks.Clear();
forwardedHeadersOptions.KnownProxies.Clear();
app.UseForwardedHeaders(forwardedHeadersOptions);
var rewriteOptions = new RewriteOptions().AddRedirectToHttps(307);
app.UseRewriter(rewriteOptions);
}
// more stuff
}
Why you ask? You shall find the answer in Heroku’s documentation:
Under the hood, Heroku router (over)writes the X-Forwarded-Proto and the X-Forwarded-Port request headers. The app must check X-Forwarded-Proto and respond with a redirect response when it is not https but http.
That is what the code above does. And then finally, we redirect to https using a 307 status for internal redirects 🔐.
And that’s it! You should now be able to focus on developing (or procrastinating) your awesome hobby project. Best of luck!