I’ve recently been working with Docker deployments to get my ASP.NET Core applications into the hands of more developers. The whole experience has been generally positive, and it is reminiscent of the early days of Git deployments. Write some code, test it locally, push changes, and voila! I have an application running in the cloud, only this time, there’s no need to check whether the cloud environment has any particular dependencies. However, like any approach, there are going to be some downsides that we should consider.

In this post, I’ll describe some hurdles of deploying a containerized application to the cloud and some solutions to working around these particular annoyances.

Exposing Port 80

ASP.NET Core templates have a prepackaged Dockerfile that’s a great starting point for most web applications. When you decide to add Docker support, you get the base image selection according to your chosen target framework. You will also receive the additional steps of building, publishing, and running your application for your final image.

FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["Sample/Sample.csproj", "Sample/"]
RUN dotnet restore "Sample/Sample.csproj"
COPY . .
WORKDIR "/src/Sample"
RUN dotnet build "Sample.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "Sample.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Sample.dll"]

The issue I found is most container hosting environments will only need port 80 exposed. The additional exposure of port 443 is unnecessary in most cases. Depending on where you host your applications, the production environment’s load-balancer will handle SSL-based traffic and forward requests to Docker containers. It’s also a complete pain in the butt to package and use SSL certificates in a Docker container.

Before pushing your code changes to your Git repository, be sure to remove the following line from your Dockerfile.

EXPOSE 443

Depending on your hosting environment, it will also make discovering which exposed port on your container is listening for HTTP traffic.

Configuration Settings as Environment Variables

Once containerized applications start, there’s not much we can do to affect their state in most cloud hosting environments. Some hosting services offer SSH directly into containers to run arbitrary commands, but that defeats the point of containerization. No, we need to get all configurations right when our application starts for those of us who’ve chosen the container lifestyle.

Environment Variables become crucial for many containerized applications, and luckily for ASP.NET Core developers, we have access to a plethora of configuration providers, one being the EnvironmentVariablesConfigurationProvider. First, let’s understand how configuration values work. Configuration provider values stack on top of each other, with each new provider having the ability to add or replace an existing value. The stacking capability is a powerful feature in realizing the configuration context of an application. Still, with containers, we heavily lean on Environment Variables to affect the more dynamic values, such as API secrets, database connection strings, and other sensitive information.

There are some caveats when setting environment variables you should be aware of:

  • Environment variable keys on the host may be case-sensitive in some environments, but configuration keys in an ASP.NET application are case-insensitive. As a result, the behavior may cause the loading process to overwrite values and squash keys.
  • Hierarchical keys can be separated with a colon (:). However, Bash, the scripting language, does not support the colon identifier. You may use the double underscore (__) instead. For example, Space:ClientId and Space__ClientId will resolve to the same key in your ASP.NET application.
  • When building a container, you can specify environment variables as part of the containerization process, which you can add to the Entrypoint command.
  • On Linux, some environment variables will need to be escaped, especially those such as URLs. You can use the systemd-escape command to set pesky environment variables safely.

As ASP.NET developers, you’re likely used to altering XML and JSON configuration files. However, as you containerize more apps, you’ll need to become comfortable with environment variables.

Learn more about environment variables at the Microsoft documentation site.

Console Logging

Console logging is a right of passage for anyone building an ASP.NET Core application. When the application processes an incoming HTTP request, the app writes the results to the console output. It works great for a development workflow. Sadly, the console output performance is notoriously terrible, and it will lock the process to flush output to the console. As tempting as it might be to leave console logging enabled, I highly recommend you take one of a few steps to improve the performance of your application profile.

  • Turn off console logging entirely.
  • Limit the logging to console to more exceptional levels like Warning or Error.
  • Remove console logging and Choose another logging mechanism that is asynchronous and non-blocking.

Your users will thank you with this simple performance hack.

Configuration Reloads and FileSystemWatcher

We’ve talked about configuration before, but did you know that many of the configuration providers have support for hot-reloading? So if you were to add, remove, or change any configuration file, you see those changes reflected in your application without a restart. The hot reload of configuration values is a neat feature for folks during development or folks self-hosting their apps. However, as you may have guessed, this feature isn’t helpful when we build images since we aren’t planning on changing any configuration values outside of environment variables.

The way the hot reload works is it uses Linux’s inotify feature to listen for file changes. Still, the number of file listeners is usually limited, depending on your base image and host environment. The limitation may cause some unexpected errors related to .NET’s FileSystemWatcher when starting containers.

Unhandled Exception: System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached.
 at System.IO.FileSystemWatcher.StartRaisingEvents()
 at System.IO.FileSystemWatcher.StartRaisingEventsIfNotDisposed()
 at System.IO.FileSystemWatcher.set_EnableRaisingEvents(Boolean value)
 at Microsoft.Extensions.FileProviders.Physical.PhysicalFilesWatcher.TryEnableFileSystemWatcher()
 at Microsoft.Extensions.FileProviders.Physical.PhysicalFilesWatcher.CreateFileChangeToken(String filter)
 at Microsoft.Extensions.FileProviders.PhysicalFileProvider.Watch(String filter)
 at Microsoft.Extensions.Configuration.FileConfigurationProvider.<.ctor>b__0_0()
 at Microsoft.Extensions.Primitives.ChangeToken.OnChange(Func`1 changeTokenProducer, Action changeTokenConsumer)
 at Microsoft.Extensions.Configuration.FileConfigurationProvider..ctor(FileConfigurationSource source)
 at Microsoft.Extensions.Configuration.Json.JsonConfigurationSource.Build(IConfigurationBuilder builder)
 at Microsoft.Extensions.Configuration.ConfigurationBuilder.Build()
 at Microsoft.AspNetCore.Hosting.WebHostBuilder.BuildCommonServices(AggregateException& hostingStartupErrors)
 at Microsoft.AspNetCore.Hosting.WebHostBuilder.Build()

Have no fear; there’s an easy workaround to this issue. Remember our old friend, the Environment Variable? An environment variable allows us to switch the behavior from relying on the system behavior and switching back to file polling.

DOTNET_USE_POLLING_FILE_WATCHER=1

We can also use a DOTNET environment variable to disable reload on config completely.

DOTNET_HOSTBUILDER__RELOADCONFIGONCHANGE=false

Use the one that’s appropriate for your use case.

Auth and HTTP Headers

Our final tip goes back to SSL certificates and handling authentication providers properly in our containerize ASP.NET Core applications. Typically, load balancers will use HTTP headers to forward the URL information our users used to access our application, rather than the internal URL information given to our application when we started it.

To have authentication providers and route generation work properly within ASP.NET Core applications, we need to set the ForwardedHeadersOptions and use the ForwardedHeadersMiddleware to change the HttpRequest properties used by the ASP.NET pipeline.

The first step is to configure the ForwardedHeadersOptions in our ServicesCollection. The below code uses ASP.NET Core’s minimal API interface, but this will also work with ASP.NET Core MVC apps.

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = 
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    options.KnownNetworks.Clear();
    options.KnownProxies.Clear();
});

We need to clear KnownNetworks and KnownProxies to ensure that ASP.NET Core uses or forwarded headers exclusively to generate URLs. We then need to register the ForwardedHeadersMiddleware into our request pipeline before other middleware.

app.UseForwardedHeaders();
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
app.MapRazorPages();

Now we can successfully handle callbacks to our container and generate URLs correctly. I want to thank Maarten Balliauw for helping with this particular tip.

Conclusion

Hosting ASP.NET Core applications inside a container has many benefits, the biggest allowing you to choose what your environment is. In my case, I’ve been running .NET 6 preview workloads long before .NET 6’s release to the public. It allows me to learn and plan for a future that’s still unfolding. While it has benefits, there are challenges, and I hope I’ve outlined some of the common issues you may run into when containerizing your applications.

If any of these tips helped you overcome any particular hurdle, please let me know on Twitter @buhakmeh and consider buying me a coffee.

As always, thanks for reading.