ASP.NET Core in Action 27 Publishing and deploying your application

27 Publishing and deploying your application

This chapter covers
• Publishing an ASP.NET Core application
• Hosting an ASP.NET Core application in IIS
• Customizing the URLs for an ASP.NET Core app

We’ve covered a vast amount of ground so far in this book. We’ve gone over the basic mechanics of building an ASP.NET Core application, such as configuring dependency injection (DI), loading app settings, and building a middleware pipeline. We’ve looked at building APIs using minimal APIs and web API controllers. We’ve looked at the server-rendered UI side, using Razor templates and layouts to build an HTML response. And we’ve looked at higher-level abstractions, such as Entity Framework Core (EF Core) and ASP.NET Core Identity, that let you interact with a database and add users to your application. In this chapter we’re taking a slightly different route. Instead of looking at ways to build bigger and better applications, we’ll focus on what it means to deploy your application so that users can access it.

We’ll start by looking again at the ASP.NET Core hosting model in section 27.1 and examining why you might want to host your application behind a reverse proxy instead of exposing your app directly to the internet. I show you the difference between running an ASP.NET Core app in development using dotnet run and publishing the app for use on a remote server. Finally, I describe some of the options available when you’re deciding how and where to deploy your app.

In section 27.2 I show you how to deploy your app to one such option: a Windows server running Internet Information Services (IIS). This is a typical deployment scenario for many developers who are familiar with the legacy .NET Framework version of ASP.NET, so it acts as a useful case study, but it’s certainly not the only possibility. I don’t go into all the technical details of configuring the venerable IIS system; instead, I show you the bare minimum required to get it up and running. If your focus is cross-platform development, don’t worry, because I don’t dwell on IIS for too long.

In section 27.3 I provide an introduction to hosting on Linux. You’ll see how it differs from hosting applications on Windows, learn the changes you need to make to your apps, and find out about some gotchas to look out for. I describe how reverse proxies on Linux differ from IIS and point you to some resources you can use to configure your environments rather than give exhaustive instructions in this book.

If you’re not hosting your application using IIS, you’ll likely need to set the URL that your ASP.NET Core app is using when you deploy your application. In section 27.4 I show two approaches: using the special ASPNETCORE_URLS environment variable and using command-line arguments. Although this task generally is not a problem during development, setting the correct URLs for your app is critical when you need to deploy it.

This chapter covers a relatively wide array of topics, all related to deploying your app. But before we get into the nitty-gritty, I’ll go over the hosting model for ASP.NET Core so that we’re on the same page. This is significantly different from the hosting model of the legacy version of ASP.NET, so if you’re coming from that background, it’s best to try to forget what you know!

27.1 Understanding the ASP.NET Core hosting model

If you think back to part 1 of this book, you may remember that we discussed the hosting model of ASP.NET Core. ASP.NET Core applications are, essentially, console applications. They have a static void Main function that is the entry point for the application, as a standard .NET console app would.

NOTE The entry point for programs using top-level statements is automatically generated by the compiler. It’s not called Main (it typically has an “invalid” name, such as

$), but otherwise it has the same signature as the classic static void Main function you would write by hand.

What makes a .NET app an ASP.NET Core app is that it runs a web server, typically Kestrel, inside the console app process. Kestrel provides the HTTP functionality to receive requests and return responses to clients. Kestrel passes any requests it receives to the body of your application and generates a response, as shown in figure 27.1. This hosting model decouples the server and reverse proxy from the application itself so that the same application can run unchanged in multiple environments.

alt text

Figure 27.1 The hosting model for ASP.NET Core gives flexibility. The same application can run exposed directly to the network, behind various reverse proxies without modification, and even inside the IIS process.

In this book we’ve focused on the “application” part of figure 27.1—the ASP.NET Core application itself—but the reality is that sometimes you’ll want to place your ASP.NET Core apps behind a reverse proxy, such as IIS in Windows or NGINX or Apache in Linux. The reverse proxy is the program that listens for HTTP requests from the internet and then makes requests to your app as though the request came from the internet directly.

DEFINITION A reverse proxy is software that’s responsible for receiving requests and forwarding them to the appropriate web server. The reverse proxy is exposed directly to the internet, whereas the underlying web server is exposed only to the proxy.

If you’re running your application using a Platform as a Service (PaaS) offering such as Azure App Service, you’re using a reverse proxy there too—one that is managed by Azure. Using a reverse proxy has many benefits:

• Security—Reverse proxies are specifically designed to be exposed to malicious internet traffic, so they’re typically extremely well-tested and battle-hardened.
• Performance—You can configure reverse proxies to provide performance improvements by aggressively caching responses to requests.
• Process management—An unfortunate reality is that apps sometimes crash. Some reverse proxies can act as monitors/schedulers to ensure that if an app crashes, the proxy can automatically restart it.
• Support for multiple apps—It’s common to have multiple apps running on a single server. Using a reverse proxy makes it easier to support this scenario by using the host name of a request to decide which app should receive the request.

I don’t want to make it seem like using a reverse proxy is all sunshine and roses. There are some downsides:

• Complexity—One of the biggest complaints is how complex reverse proxies can be. If you’re managing the proxy yourself (as opposed to relying on a PaaS implementation), there can be lots of proxy-specific pitfalls to look out for.
• Inter-process communication—Most reverse proxies require two processes: a reverse proxy and your web app. Communicating between the two is often slower than if you directly exposed your web app to requests from the internet.
• Restricted features—Not all reverse proxies support all the same features as an ASP.NET Core app. For example, Kestrel supports HTTP/2, but if your reverse proxy doesn’t, you won’t see the benefits.

Whether you choose to use a reverse proxy or not, when the time comes to host your app, you can’t copy your code files directly to the server. First, you need to publish your ASP.NET Core app to optimize it for production. In section 27.1.1 we’ll look at building an ASP.NET Core app so that it can be run on your development machine, compared with publishing it so that it can be run on a server.

27.1.1 Running vs. publishing an ASP.NET Core app

One of the key changes in ASP.NET Core from previous versions of ASP.NET is making it easy to build apps using your favorite code editors and integrated development environments (IDEs). Previously, Visual Studio was required for ASP.NET development, but with the .NET command-line interface (CLI), you can build apps with the tools you’re comfortable with on any platform.

As a result, whether you build using Visual Studio or the .NET CLI, the same tools are being used under the hood. Visual Studio provides an additional graphical user interface (GUI), functionality, and wrappers for building your app, but it (mostly) executes the same commands as the .NET CLI behind the scenes.

As a refresher, you’ve used four main .NET CLI commands so far to build your apps:

• dotnet new—Creates an ASP.NET Core application from a template
• dotnet restore—Downloads and installs any referenced NuGet packages for your project
• dotnet build—Compiles and builds your project
• dotnet run—Executes your app so you can send requests to it

If you’ve ever built a .NET application, whether it’s a legacy ASP.NET app or a .NET Framework console app, you’ll know that the output of the build process is written to the bin folder by default. The same is true for ASP.NET Core applications.

If your project compiles successfully when you call dotnet build, the .NET CLI writes the artifacts to a bin folder in your project’s directory. Inside this bin folder are several files required to run your app, including a .dll file that contains the code for your application. Figure 27.2 shows the output of the bin folder for a basic ASP.NET Core application.

alt text

Figure 27.2 The bin folder for an ASP.NET Core app after running dotnet build. The application is compiled into a single .dll file, ExampleApp.dll.

NOTE In Windows you also have an executable .exe file, ExampleApp.exe. This is a simple wrapper file for convenience that makes it easier to run the application contained in ExampleApp.dll.

When you call dotnet run in your project folder (or run your application using Visual Studio), the .NET CLI uses the .dll to run your application. But this file doesn’t contain everything you need to deploy your app.

To host and deploy your app on a server, you first need to publish it. You can publish your ASP.NET Core app from the command line using the dotnet publish command, which builds and packages everything your app needs to run. The following command packages the app from the current directory and builds it to a subfolder called publish. I’ve used the Release configuration instead of the default Debug configuration so that the output will be fully optimized for running in production:

dotnet publish --output publish --configuration Release

TIP Always use the Release configuration when publishing your app for deployment. This ensures that the compiler generates optimized code for your app.

Once the command completes, you’ll find your published application in the publish folder, as shown in figure 27.3.

alt text

Figure 27.3 The publish folder for the app after running dotnet publish. The app is still compiled into a single .dll file, but all the additional files, such as wwwroot, are also copied to the output.

As you can see, the ExampleApp.dll file is still there, along with some additional files. Most notably, the publish process has copied across the wwwroot folder of static files. When running your application locally with dotnet run, the .NET CLI uses these files from your application’s project folder directly. Running dotnet publish copies the files to the output directory, so they’re included when you deploy your app to a server.

If your first instinct is to try running the application in the publish folder using the dotnet run command you already know and love, you’ll be disappointed. Instead of seeing the application starting up, you’ll see a somewhat confusing message: Couldn’t find a project to run.

To run a published application, you need to use a slightly different command. Instead of calling dotnet run, you must pass the path to your application’s .dll file to the dotnet command. If you’re running the command from the publish folder, for the example app in figure 27.3, it would look something like

dotnet ExampleApp.dll

This is the command that your server will run when running your application in production.

TIP You can also use the dotnet exec command to achieve the same thing, such as dotnet exec ExampleApp.dll. This makes some advanced runtime options available, as described in the docs at http://mng.bz/x4d8.

When you’re developing, the dotnet run command does a whole load of work to make things easier on you. It makes sure that your application is built, looks for a project file in the current folder, works out where the corresponding .dlls will be (in the bin folder), and finally runs your app.

In production, you don’t need any of this extra work. Your app is already built; it only needs to be run. The dotnet <dll> syntax does this alone, so your app starts much faster.

NOTE The dotnet command used to run your published application is part of the .NET Runtime. The (identically named) dotnet command used to build and run your application during development is part of the .NET software development kit (SDK).

Framework-dependent deployments vs. self-contained deployments
.NET Core applications can be deployed in two ways: runtime-dependent deployments (RDD) and self-contained deployments (SCD).

By default, you’ll use an RDD. This relies on the .NET 7 runtime being installed on the target machine that runs your published app, but you can run your app on any platform—Windows, Linux, or macOS—without having to recompile.

By contrast, an SCD contains all the code required to run your app, so the target machine doesn’t need to have .NET 7 installed. Instead, publishing your app packages up the .NET 7 runtime with your app’s code and libraries.

Each approach has its pros and cons, but in most cases I tend to create RDDs. The final size of RDDs is much smaller, as they contain only your app code instead of the whole .NET 7 framework, which SCDs contain. Also, you can deploy your RDD apps to any platform, whereas SCDs must be compiled specifically for the target machine’s operating system, such as Windows 10 64-bit or Red Hat Enterprise Linux 64-bit.

That said, SCDs are excellent for isolating your application from dependencies on the hosting machine. SCDs don’t rely on the version of .NET installed on a hosting provider, so you can (for example) use preview versions of .NET in Azure App Service without needing the preview version to be supported.

Another advantage of SCDs is for regulated industries that require certification or procedure to change applications. In RDDs (such as in Azure App Service) the underlying runtime may be patched at any time without your intervention, potentially leading to noncompliance. With SCDs, your app contains a fixed runtime and can be considered an immutable snapshot of your app. Of course, that means you must make sure to patch the runtime of your SCDs manually, performing regular deployments. Patch versions of the .NET runtime are generally released every month, so make sure to plan for at least monthly releases of your SCD apps.

In this book I discuss RDDs only for simplicity, but if you want to create an SCD, provide a runtime identifier (in this case, Windows 10 64-bit) when you publish your app:

dotnet publish -c Release -r win10-x64 --self-contained -o publish_folder

The output will contain an .exe file, which is your application, and a ton of .dlls (about 100 MB of .dlls for a default sample app), which are the .NET 7 framework. You need to deploy this whole folder to the target machine to run your app. Note that you need to publish for a specific operating system and architecture. The list of available runtime identifiers is available in the documentation at http://mng.bz/Aolp.

In .NET 7 it’s possible to trim these assemblies during the publish process, but this comes with risks in some scenarios. You can also bundle this folder into a single file automatically for easier deployments. For more details, see Microsoft’s “.NET application publishing overview” documentation at https://learn.microsoft.com/dotnet/core/deploying.

We’ve established that publishing your app is important for preparing it to run in production, but how do you go about deploying it? How do you get the files from your computer onto a server so that people can access your app? You have many, many options, so in the next section I’ll give you a brief list of approaches to consider.

27.1.2 Choosing a deployment method for your application

To deploy any application to production, you generally have two fundamental requirements:

• A server that can run your app
• A means of loading your app onto the server

Historically, putting an app into production was a laborious and error-prone process. For many people, this is still true. If you’re working at a company that hasn’t changed practices in recent years, you may need to request a server or virtual machine for your app and provide your application to an operations team that will install it for you. If that’s the case, you may have your hands tied regarding how you deploy.

For those who have embraced continuous integration (CI) or continuous delivery/deployment (CD), there are many more possibilities. CI/CD is the process of detecting changes in your version control system (for example, Git, SVN, Mercurial, or Team Foundation Version Control) and automatically building, and potentially deploying, your application to a server with little to no human intervention.

NOTE There are important but subtle differences between these terms. Atlassian has a good comparison article, “Continuous integration vs. continuous delivery vs. continuous deployment,” at http://mng.bz/vzp4.

There are many CI/CD systems out there—Azure DevOps, GitHub Actions, Jenkins, TeamCity, AppVeyor, Travis, and Octopus Deploy, to name a few. Each can manage some or all of the CI/CD process and can integrate with many systems.

Rather than push any particular system, I suggest trying some of the services available and seeing which works best for you. Some are better suited to open-source projects, and some are better when you’re deploying to cloud services; it all depends on your particular situation.

If you’re getting started with ASP.NET Core and don’t want to have to go through the setup process of getting CI working, you still have lots of options. The easiest way to get started with Visual Studio is to use the built-in deployment options. These are available from Visual Studio via the Build > Publish <AppName> command, which presents the screen shown in figure 27.4.

alt text

Figure 27.4 The Publish application screen in Visual Studio 2022. This provides easy options for publishing your application directly to Azure App Service, to IIS, to an FTP site, or to a folder on the local machine.

From here, you can publish your application directly from Visual Studio to many locations. This is great when you’re getting started, though I recommend looking at a more automated and controlled approach when you have a larger application or a whole team working on a single app.

TIP For guidance on choosing your Visual Studio publishing options, see Microsoft’s “Deploy your app to a folder, IIS, Azure, or another destination” documentation at http://mng.bz/4Z8j.

Given the number of possibilities available in this space and the speed with which these options change, I’m going to focus on one specific scenario in this chapter: you’ve built an ASP.NET Core application, and you need to deploy it. You have access to a Windows server that’s already serving legacy .NET Framework ASP.NET applications using IIS, and you want to run your ASP.NET Core app alongside them.

In the next section you’ll see an overview of the steps required to run an ASP.NET Core application in production, using IIS as a reverse proxy. It won’t be a master class in configuring IIS (there’s so much depth to the 25-year-old product that I wouldn’t know where to start!), but I’ll cover the basics needed to get your application serving requests.

27.2 Publishing your app to IIS

In this section I briefly show you how to publish your first app to IIS. You’ll add an application pool and website to IIS and ensure that your app has the necessary configuration to work with IIS as a reverse proxy. The deployment itself will be as simple as copying your published app to IIS’s hosting folder.

In section 27.1 you learned about the need to publish an app before you deploy it and the benefits of using a reverse proxy when you run an ASP.NET Core app in production. If you’re deploying your application to Windows, IIS will likely be your reverse proxy and will be responsible for managing your application.

IIS is an old and complex beast, and I can’t possibly cover everything related to configuring it in this book. Neither would you want me to; that discussion would be boring! Instead, in this section I’ll provide an overview of the basic requirements for running ASP.NET Core behind IIS, along with the changes you may need to make to your application to support IIS.

If you’re on Windows and want to try these steps locally, you’ll need to enable IIS manually on your development machine. If you’ve done this with older versions of Windows, nothing much has changed. You can find a step-by-step guide to configuring IIS and troubleshooting tips in the ASP.NET Core documentation at http://mng.bz/6g2R.

27.2.1 Configuring IIS for ASP.NET Core

The first step in preparing IIS to host ASP.NET Core applications is installing the ASP.NET Core Windows Hosting Bundle (http://mng.bz/opED). This includes several components needed to run .NET apps:

• The .NET Runtime—Runs your .NET 7 application
• The ASP.NET Core Runtime—Required to run ASP.NET Core apps
• The IIS AspNetCore Module—Provides the link between IIS and your app so that IIS can act as a reverse proxy

If you’re going to be running IIS on your development machine, make sure that you install the bundle as well; otherwise, you’ll get strange errors from IIS.

TIP The Windows Hosting Bundle provides everything you need for running ASP.NET Core behind IIS in Windows. If you’re hosting your application in Linux or Mac, or aren’t using IIS in Windows, you need to install only the .NET Runtime and ASP.NET Core Runtime to run runtime-dependent ASP.NET Core apps. Note that you need to install the IIS AspNetCore Module even if you are using SCDs.

Once you’ve installed the bundle, you need to configure an application pool in IIS for your ASP.NET Core apps. Previous versions of ASP.NET would run in a managed app pool that used .NET Framework, but for ASP.NET Core you should create a No Managed Code pool. The native ASP.NET Core Module runs inside the pool, which boots the .NET 7 Runtime itself.

DEFINITION An application pool in IIS represents an application process. You can run each app in IIS in a separate application pool to keep the apps isolated from one another.

To create an unmanaged application pool, right-click Application Pools in IIS and choose Add Application Pool from the contextual menu. Provide a name for the app pool in the resulting dialog box, such as dotnet7, and set the .NET CLR version to No Managed Code, as shown in figure 27.5.

alt text

Figure 27.5 Creating an app pool in IIS for your ASP.NET Core app. The .NET CLR version should be set to No Managed Code.

Now that you have an app pool, you can add a new website to IIS. Right-click the Sites node, and choose Add Website from the contextual menu. In the Add Website dialog box, shown in figure 27.6, you provide a name for the website and the path to the folder where you’ll publish your website. I created a folder that I’ll use to deploy the Recipe app from previous chapters. It’s important to change the Application Pool for the app to the new dotnet7 app pool you created. In production, you’d also provide a hostname for the application, but I’ve left it blank for now in this example and changed the port to 81 so the application will bind to the URL http://localhost:81.

NOTE When you deploy an application to production, you need to register a hostname with a domain registrar so that your site is accessible by people on the internet. Use that hostname when configuring your application in IIS, as shown in figure 27.6.

alt text

Figure 27.6 Adding a new website to IIS for your app. Be sure to change the Application Pool to the No Managed Code pool created in the previous step. You also provide a name, the path where you’ll publish your app files, and the URL that IIS will use for your app.

Once you click OK, IIS creates the application and attempts to start it. But you haven’t published your app to the folder, so you won’t be able to view it in a browser yet.

You need to carry out one more critical setup step before you can publish and run your app: grant permissions for the dotnet7 app pool to access the path where you’ll publish your app. To do this, right-click the folder that will host your app in Windows File Explorer, and choose Properties from the contextual menu. In the Properties dialog box, choose Security > Edit > Add. Enter IIS AppPool\dotnet7 in the text box, as shown in figure 27.7, where dotnet7 is the name of your app pool; then choose OK. Close all the dialog boxes by choosing OK, and you’re all set.

alt text

Figure 27.7 Adding permission for the dotnet7 app pool to the website’s publish folder

Out of the box, the ASP.NET Core templates are configured to work seamlessly with IIS, but if you’ve created an app from scratch, you may need to make a couple of changes. In the next section I’ll briefly show the changes you need to make and explain why they’re necessary.

27.2.2 Preparing and publishing your application to IIS

As I discussed in section 27.1, IIS acts as a reverse proxy for your ASP.NET Core app. That means IIS needs to be able to communicate directly with your app to forward incoming requests to and outgoing responses from your app.

IIS handles this with the ASP.NET Core Module, but a certain degree of negotiation is required between IIS and your app. For this to work correctly, you need to configure your app to use IIS integration.

IIS integration is added automatically when you use WebApplicationBuilder, so there’s typically nothing more to do. However, in chapter 30 you’ll learn about the generic host and how to create custom application builders using HostBuilder. If your app uses a customer application builder and you want to use IIS, you need to ensure that you add IIS integration with the UseIIS() or UseIISIntegration() extension methods:

• UseIIS() configures your application to support IIS with an in-process hosting model.
• UseIISIntegration() configures your application to support IIS with an out-of-process hosting model.

These methods are automatically called by WebApplicationBuilder, but if you’re not using your application with IIS, the UseIIS() and UseIISIntegration() methods will have no effect on your app, so it’s safe to include them anyway.

In-process vs. out-of-process hosting in IIS
The common reverse-proxy description assumes that your application is running in a separate process from the reverse proxy itself. That is the case if you’re running on Linux and was the default for IIS up until ASP.NET Core 3.0.

In ASP.NET Core 3.0, ASP.NET Core switched to using an in-process hosting model by default for applications deployed to IIS. In this model, IIS hosts your application directly inside the IIS process, reducing interprocess communication and boosting performance.

You can switch to the out-of-process hosting model with IIS if you wish, which can sometimes be useful for troubleshooting problems. Rick Strahl has an excellent post on the differences between the hosting models, how to switch between them, and the advantages of each: “ASP.NET Core In Process Hosting on IIS with ASP.NET Core” at http://mng.bz/QmEv.

In general, you shouldn’t need to worry about the differences between the hosting models, but it’s something to be aware of if you’re deploying to IIS. If you choose to use the out-of-process hosting model, you should use the UseIISIntegration() extension method. If you use the in-process model, use UseIIS(). Alternatively, play it safe and use both; the correct extension method is activated based on the hosting model used in production. Neither extension does anything if you don’t use IIS.

When running behind IIS, these extension methods configure your app to pair with IIS so that it can seamlessly accept requests. Among other things, the extensions do the following:

• Define the URL that IIS uses to forward requests to your app and configures your app to listen on this URL
• Configure your app to interpret requests coming from IIS as coming from the client by setting up header forwarding
• Enable Windows authentication if required

Adding the IIS extension methods is the only change you need to make to your application to host in IIS (and even then, only when using a custom application builder). But there’s one additional aspect to be aware of when you publish your app. As with legacy .NET Framework ASP.NET, IIS relies on a web.config file to configure the applications it runs. It’s important that your application include a web.config file when it’s published to IIS; otherwise you could get broken behavior or even expose files that shouldn’t be exposed.

TIP For details on using web.config to customize the IIS AspNetCore Module, see Microsoft’s “ASP.NET Core Module” documentation: http://mng.bz/Xdna.

If your ASP.NET Core project already includes a web.config file, the .NET CLI or Visual Studio copies it to the publish directory when you publish your app. If your app doesn’t include a web.config file, the publish command creates the correct one for you. If you don’t need to customize the web.config file, it’s generally best not to include one in your project and let the CLI create the correct file for you.

With these changes, you’re finally in a position to publish your application to IIS. Publish your ASP.NET Core app to a folder, either from Visual Studio or with the .NET CLI, by running

dotnet publish --output publish_folder --configuration Release

This will publish your application to the publish_folder folder. You can then copy your application to the path specified in IIS, as shown in figure 27.6. At this point, if all has gone smoothly, you should be able to navigate to the URL you specified for your app (http://localhost:81, in my case) and see it running, as shown in figure 27.8.

alt text

Figure 27.8 The published application, using IIS as a reverse proxy listening at the URL http://localhost:81

And there you have it—your first application running behind a reverse proxy. Even though ASP.NET Core uses a different hosting model from previous versions of ASP.NET, the process of configuring IIS is similar.

As is often the case when it comes to deployment, the success you have is highly dependent on your precise environment and your app itself. If, after following these steps, you find that you can’t get your application to start, I highly recommend checking out the documentation at http://mng.bz/Zqom. This contains many troubleshooting steps to get you back on track if IIS decides to throw a hissy fit.

This section was deliberately tailored to deploying to IIS, as it provides a great segue for developers who are used to deploying legacy ASP.NET apps and want to deploy their first ASP.NET Core app. But that’s not to say that IIS is the only, or best, place to host your application.

In the next section I provide a brief introduction to hosting your app on Linux, behind a reverse proxy like NGINX or Apache. I won’t go into configuration of the reverse proxy itself, but I will provide an overview of things to consider and resources you can use to run your applications on Linux.

27.3 Hosting an application in Linux

One of the great new features in ASP.NET Core is the ability to develop and deploy applications cross-platform, whether on Windows, Linux, or macOS. The ability to run on Linux in particular opens the possibility of cheaper deployments to cloud hosting, deploying to small devices like a Raspberry Pi or to Docker containers.

One of the characteristics of Linux is that it’s almost infinitely configurable. Although that’s definitely a feature, it can also be extremely daunting, especially if you’re coming from the Windows world of wizards and GUIs. This section provides an overview of what it takes to run an application on Linux. It focuses on the broad steps you need to take rather than the somewhat-tedious details of the configuration itself. Instead, I point to resources you can refer to as necessary.

27.3.1 Running an ASP.NET Core app behind a reverse proxy in Linux

You’ll be glad to hear that running your application on Linux is broadly the same as running your application on Windows with IIS:

  1. Publish your app using dotnet publish. If you’re creating an RDD, the output is the same as you’d use with IIS. For an SCD, you must provide the runtime identifier, as described in section 27.1.1.
  2. Install the necessary prerequisites on the server. For an RDD deployment, you must install the .NET 7 Runtime and the necessary prerequisites. You can find details on this in Microsoft’s “Install .NET on Linux” documentation at http://mng.bz/Rxlj.
  3. Copy your app to the server. You can use any mechanism you like: FTP, USB stick, or whatever you need to get your files onto the server!
  4. Configure a reverse proxy, and point it to your app. As you know by now, you may want to run your app behind a reverse proxy, for the reasons described in section 27.1. In Windows you’d use IIS, but in Linux you have more options. NGINX, Apache, and HAProxy are commonly used options. The ASP.NET Core-based YARP is also an option (https://microsoft.github.io/reverse-proxy). Alternatively, go without, and expose your app directly to the network.
  5. Configure a process-management tool for your app. In Windows, IIS acts as both a reverse proxy and a process manager, restarting your app if it crashes or stops responding. In Linux, you typically need to configure a separate process manager to handle these duties; the reverse proxies won’t do them for you.

The first three steps are generally the same, whether you’re running in Windows with IIS or in Linux, but the last two steps are more interesting. By contrast with the monolithic IIS, Linux has a philosophy of small applications, each with a single responsibility.

IIS runs on the same server as your app and takes on multiple duties—proxying traffic from the internet to your app, but also monitoring the app process itself. If your app crashes or stops responding, IIS restarts the process to ensure that you can keep handling requests.

In Linux, the reverse proxy might be running on the same server as your app, but it’s also common for it to be running on a different server, as shown in figure 27.9. This is similarly true if you choose to deploy your app to Docker; your app would typically be deployed in a container without a reverse proxy, and a reverse proxy on a server would point to your Docker container.

alt text

Figure 27.9 In Linux, it’s common for a reverse proxy to be on a different server from your app. The reverse proxy forwards incoming requests to your app, while a process manager, such as systemd, monitors your apps for crashes and restarts it as appropriate.

As the reverse proxies aren’t necessarily on the same server as your app, they can’t be used to restart your app if it crashes. Instead, you need to use a process manager such as systemd to monitor your app. If you’re using Docker, you typically use a container orchestrator such as Kubernetes (https://kubernetes.io) to monitor the health of your containers.

Running ASP.NET Core applications in Docker
Docker is the most commonly used engine for containerizing your applications. A container is like a small, lightweight virtual machine, specific to your app. It contains an operating system, your app, and any dependencies for your app. This container can then be run on any machine that runs Docker, and your app will run exactly the same, regardless of the host operating system and what’s installed on it. This makes deployments highly repeatable: you can be confident that if the container runs on your machine, it will run on the server too.

All the major cloud vendors have support for running containers, either standalone or as part of an orchestration service. For example, in Azure, you can run containers in Azure App Service, Azure Container Instances, Azure Container Apps, and Azure Kubernetes Service. One advantage of containers is that you can easily use the same container in all these services or even move to a different cloud provider, and your app will run the same.

ASP.NET Core is well suited to container deployments, but moving to Docker involves a big shift in your deployment methodology and may or may not be right for you and your apps. If you’re interested in the possibilities afforded by Docker and want to learn more, I suggest checking out the following resources:

• Docker in Practice, 2nd ed., by Ian Miell and Aidan Hobson Sayers (Manning, 2019) provides a vast array of practical techniques to help you get the most out of Docker (http://mng.bz/nM8d).

• Even if you’re not deploying to Linux, you can use Docker with Docker for Windows. Check out the free e-book Introduction to Windows Containers, by John McCabe and Michael Friis (Microsoft Press, 2017), at https://aka.ms/containersebook.

• You can find a lot of details on building and running your ASP.NET Core applications on Docker in the .NET documentation at http://mng.bz/vz5a.

• Steve Gordon has an excellent blog post series on Docker for ASP.NET Core developers at http://mng.bz/2Da8.

Configuring a reverse proxy and process manager on Linux is a laborious task that makes for dry reading, so I won’t detail it here. Instead, I recommend checking out the ASP.NET Core docs. They have a guide for NGINX and systemd, “Host ASP.NET Core on Linux with Nginx” (http://mng.bz/yYGd), and a guide for configuring Apache with systemd, “Host ASP.NET Core on Linux with Apache” (http://mng.bz/MXVB).

Both guides cover the basic configuration of the respective reverse proxies and systemd supervisors, but more important, they also show how to configure them securely. The reverse proxy sits between your app and the unfettered internet, so it’s important to get it right!

Configuring the reverse proxy and the process manager is typically the most complex part of deploying to Linux, and that isn’t specific to .NET development: the same would be true if you were deploying a Node.js web app. But you need to consider a few things inside your application when you’re going to be deploying to Linux, as you’ll see in the next section.

27.3.2 Preparing your app for deployment to Linux

Generally speaking, your app doesn’t care which reverse proxy it sits behind, whether it’s NGINX, Apache, or IIS; your app receives requests and responds to them without the reverse proxy affecting things. When you’re hosting behind IIS, you need UseIISIntegration() to tell your app about IIS’s configuration; when you’re hosting on Linux, you need a similar method.

When a request arrives at the reverse proxy, it contains some information that is lost after the request is forwarded to your app. For example, the original request comes with the IP address of the client/browser connecting to your app; once the request is forwarded from the reverse proxy, the IP address is that of the reverse proxy, not the browser. Also, if the reverse proxy is used for SSL/TLS offloading (see chapter 28), then a request that was originally made using HTTPS may arrive at your app as an HTTP request.

The standard solution to these problems is for the reverse proxy to add more headers before forwarding requests to your app. For example, the X-Forwarded-For header identifies the original client’s IP address, whereas the X-Forwarded-Proto header indicates the original scheme of the request (http or https).

For your app to behave correctly, it needs to look for these headers in incoming requests and modify the request as appropriate. A request to http://localhost with the X-Forwarded-Proto header set to https should be treated the same as if the request were to https://localhost.

You can use ForwardedHeadersMiddleware in your middleware pipeline to achieve this. This middleware overrides Request.Scheme and other properties on HttpContext to correspond to the forwarded headers. WebApplicationBuilder partially handles this for you; the middleware is automatically added to the pipeline in a disabled state. To enable it, set the environment variable ASPNETCORE_FORWARDEDHEADERS_ENABLED=true.

If you don’t want to use the automatically added middleware for some reason, or if you’re using the generic host (which you’ll learn about in chapter 30), you can add the middleware to the start of your middleware pipeline manually, as shown in listing 27.1, and configure it with the headers to look for.

WARNING It’s important that ForwardedHeadersMiddleware be placed early in the middleware pipeline to correct Request.Scheme before any middleware that depends on the scheme runs.

Listing 27.1 Configuring an app to use forwarded headers in Startup.cs

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();

app.UseForwardedHeaders(new ForwardedHeadersOptions      #A
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor |     #B
                       ForwardedHeaders.XForwardedProto     #B
});
app.UseHttpsRedirection();    #C
app.UseRouting();             #C
app.MapGet("/", () => "Hello world!");
app.Run();

❶ Adds ForwardedHeadersMiddleware early in your pipeline
❷ Configures the headers the middleware should look for and use
❸ The forwarded headers middleware must be placed before all other middleware.

NOTE This behavior isn’t specific to reverse proxies on Linux; the UseIis() extension adds ForwardedHeadersMiddleware under the hood as part of its configuration when your app is running behind IIS.

Aside from considering the forwarded headers, you need to consider a few minor things when deploying your app to Linux that may trip you up if you’re used to deploying to Windows alone:

• Line endings (LF in Linux versus CRLF in Windows)—Windows and Linux use different character codes in text to indicate the end of a line. This isn’t often a problem for ASP.NET Core apps, but if you’re writing text files on one platform and reading them on a different platform, it’s something to bear in mind.
• Path directory separator ("\" on Windows, "/" on Linux)—This is one of the most common bugs I see when Windows developers move to Linux. Each platform uses a different separator in file paths, so although loading a file using the "subdir\myfile.json" path will work fine in Windows, it won’t in Linux. Instead, you should use Path.Combine to create the appropriate separator for the current platform, such as Path.Combine("subdir", "myfile.json").
• ":" in environment variables—In some Linux distributions, the colon character (:) isn’t allowed in environment variables. As you saw in chapter 10, this character is typically used to denote different sections in ASP.NET Core configuration, so you often need to use it in environment variables. Instead, you can use a double underscore in your environment variables (__); ASP.NET Core will treat it the same as though you’d used a colon.
• Missing time zone and culture data—Linux distributions don’t always come with time zone or culture data, which can cause localization problems and exceptions at runtime. You can install the time zone data using your distribution’s package manager.[1] It also may be organized differently. The hierarchy of Norwegian cultures is different in Linux, for example.
• Different directory structures—Linux distributions use a different folder structure from Windows, so you need to bear that in mind if your app hardcodes paths. In particular, consider differences in temporary/cache folders.

The preceding list is not exhaustive by any means, but as long as you set up ForwardedHeadersMiddleware and take care to use cross-platform constructs like Path.Combine, you shouldn’t have too many problems running your applications on Linux. But configuring a reverse proxy isn’t the simplest of activities, so wherever you’re planning on hosting your app, I suggest checking the documentation for guidance at http://mng.bz/1qM1.

27.4 Configuring the URLs for your application

At this point, you’ve deployed an application, but there’s one aspect you haven’t configured: the URLs for your application. When you’re using IIS as a reverse proxy, you don’t have to worry about this inside your app. IIS integration with the ASP.NET Core Module works by dynamically creating a URL that’s used to forward requests between IIS and your app. The hostname you configure in IIS (in figure 27.6) is the URL that external users see for your app; the internal URL that IIS uses when forwarding requests is never exposed.

If you’re not using IIS as a reverse proxy—maybe you’re using NGINX or exposing your app directly to the internet—you may find you need to configure the URLs your application listens to directly.

By default, ASP.NET Core listens for requests on the URL http://localhost:5000. There are lots of ways to set this URL, but in this section I describe two: using environment variables or using command-line arguments. These are the two most common approaches I see (outside of IIS) for controlling which URLs your app uses.

TIP For further ways to set your application’s URL, see my “5 ways to set the URLs for an ASP.NET Core app” blog post: http://mng.bz/go0v.

In chapter 10 you learned about configuration in ASP.NET Core, and in particular about the concept of hosting environments so that you can use different settings when running in development compared with production. You choose the hosting environment by setting an environment variable on your machine called ASPNETCORE_ENVIRONMENT. The ASP.NET Core framework magically picks up this variable when your app starts and uses it to set the hosting environment.

You can use a similar special environment variable to specify the URL that your app uses; this variable is called ASPNETCORE_URLS. When your app starts up, it looks for this value and uses it as the application’s URL. By changing this value, you can change the default URL used by all ASP.NET Core apps on the machine. For example, you could set a temporary environment variable in Windows from the command window using

set ASPNETCORE_URLS=http://localhost:8000

Running a published application using dotnet within the same command window, as shown in figure 27.10, shows that the app is now listening on the URL provided in the ASPNETCORE_URLS variable.

alt text
Figure 27.10 Change the ASPNETCORE_URLS environment variable to change the URL used by ASP.NET Core apps.

You can instruct an app to listen on multiple URLs by separating them with a semicolon, or you can listen to a specific port without specifying the localhost hostname. If you set the ASPNETCORE_URLS environment variable to
http://localhost:5001;http://*:5002

your ASP.NET Core apps will listen for requests sent to the following:

http://localhost:5001—This address is accessible only on your local computer, so it will not accept requests from the wider internet.
http://*:5002—Any URL on port 5002. External requests from the internet can access the app on port 5002, using any URL that maps to your computer.

Note that you can’t specify a different hostname, like tastyrecipes.com. ASP.NET Core listens to all requests on a given port; it doesn’t listen for specific domain names. The exception is the localhost hostname, which allows only requests that came from your own computer.

NOTE If you find the ASPNETCORE_URLS variable isn’t working properly, ensure that you don’t have a launchSettings.json file in the directory. When present, the values in this file take precedence. By default, launchSettings.json isn’t included in the publish output, so this generally won’t be a problem in production.

Setting the URL of an app using a single environment variable works great for some scenarios, most notably when you’re running a single application in a virtual machine, or within a Docker container.

TIP ASP.NET Core is well suited to running in containers but working with containers is a separate book in its own right. For details on hosting and publishing apps using Docker, see Microsoft’s “Host ASP.NET Core in Docker containers” documentation: http://mng.bz/e5GV.

If you’re not using Docker containers or a PaaS offering, chances are that you’re hosting multiple apps side-by-side on the same machine. A single environment variable is no good for setting URLs in this case, as it would change the URL of every app.

In chapter 10 you saw that you could set the hosting environment using the ASPNETCORE_ENVIRONMENT variable, but you could also set the environment using the --environment flag when calling dotnet run:

dotnet run --no-launch-profile --environment Staging

You can set the URLs for your application in a similar way, using the --urls parameter. Using command-line arguments enables you to have multiple ASP.NET Core applications running on the same machine, listening to different ports. For example, the following command would run the recipe application, set it to listen on port 8081, and set the environment to staging (figure 27.11):

dotnet RecipeApplication.dll --urls "http://*:8081" --environment Staging

alt text

Figure 27.11 Setting the hosting environment and URLs for an application using command-line arguments. The values passed at the command line override values provided from appSettings.json or environment variables.

Remember that you don’t need to set your URLs in this way if you’re using IIS as a reverse proxy; IIS integration handles this for you. Setting the URLs is necessary only when you’re manually configuring the URL your app is listening on, such as if you’re using NGINX or are exposing Kestrel directly to clients.

WARNING If you are running your ASP.NET Core application without a reverse proxy, you should use host filtering for security reasons to ensure that your app only responds to requests for hostnames you expect. For more details, see my “Adding host filtering to Kestrel in ASP.NET Core” blog entry: http://mng.bz/pVXK.

That brings us to the end of this chapter on publishing your app. This last mile of app development—deploying an application to a server where users can access it—is a notoriously thorny problem. Publishing an ASP.NET Core application is easy enough, but the multitude of hosting options available makes providing concise steps for every situation difficult.

Whichever hosting option you choose, there’s one critical topic that you mustn’t overlook: security. In the next chapter you’ll learn about HTTPS, how to use it when testing locally, and why it’s important your production apps all use HTTPS.

27.5 Summary

ASP.NET Core apps are console applications that self-host a web server. In production, you may use a reverse proxy, which handles the initial request and passes it to your app. Reverse proxies can provide additional security, operations, and performance benefits, but they can also add complexity to your deployments.

.NET has two parts: the .NET SDK (also known as the .NET CLI) and the .NET Runtime. When you’re developing an application, you use the .NET CLI to restore, build, and run your application. Visual Studio uses the same .NET CLI commands from the IDE.

When you want to deploy your app to production, you need to publish your application, using dotnet publish. This creates a folder containing your application as a DLL, along with all its dependencies.

To run a published application, you don’t need the .NET CLI because you won’t be building the app. You need only the .NET Runtime to run a published app. You can run a published application using the dotnet app.dll command, where app.dll is the application .dll created by the dotnet publish command.

To host ASP.NET Core applications in IIS, you must install the ASP.NET Core Module. This allows IIS to act as a reverse proxy for your ASP.NET Core app. You must also install the .NET Runtime and the ASP.NET Core Runtime, which are installed as part of the ASP.NET Core Windows Hosting Bundle.

IIS can host ASP.NET Core applications using one of two modes: in-process and out-of-process. The out-of-process mode runs your application as a separate process, as is typical for most reverse proxies. The in-process mode runs your application as part of the IIS process. This has performance benefits, as no interprocess communication is required.

If you are using a custom web application builder with IIS, ensure that you call UseIISIntegration() and UseIIS() so that IIS forwards the request to your app correctly. If you’re using the default WebApplicationBuilder, these methods are called automatically for you.

When you publish your application using the .NET CLI, a web.config file is added to the output folder. It’s important that this file be deployed with your application when publishing to IIS, as it defines how your application should run.

The URL that your app listens on is specified by default using the environment variable ASPNETCORE_URLS. Setting this value changes the URL for all the apps on your machine. Alternatively, pass the --urls command-line argument when running your app, as in this example: dotnet app.dll --urls http://localhost:80.

[1] I ran into this problem myself. You can read about it in detail and how I solved it on my blog: http://mng.bz/aoem.

Leave a Reply

Your email address will not be published. Required fields are marked *