How to Deploy an ASP.NET Core 6 Application Using Nginx on Ubuntu 20.04

ยท

13 min read

The team at Microsoft, in recent years, has done an amazing job ensuring a rock-solid future for their tech stack. They pivoted from a focus on proprietary languages, tooling, and expensive licensing fees to a focus on open-source, affordable, feature-rich offerings. Don't get me wrong, there are still plenty of expensive Microsoft paywalled goodies out there...(cough, SQL Server), but I'll take what I can get when it comes to the software and tooling available for free from the folks at Microsoft these days. If you want to learn how to deploy an ASP.NET Core 6 application using Nginx on Ubuntu 20.04 then keep reading.

Disclosure: This post may contain affiliate links, meaning I get a commission if you decide to purchase through my links, at no cost to you. Please read my disclaimer for more information.

Why this combination of technologies?

I'm no different from any other developer looking to take advantage of as many free-to-use, or cheaply available technologies out there. Especially when it comes to hosting. Over the years I've moved from shared hosting to virtual private server hosting to dedicated hosting (almost went broke on that last adventure) and eventually landed on cloud hosting with virtual machines. I almost exclusively rely on Digital Ocean for my sites and projects these days because they're unbelievably affordable, and have fixed pricing which means I won't go broke or wake up to a $250 bill for running a single page website with no monthly visitors.

This isn't a gimmicky sales pitch, I use Digital Ocean for everything. It's a shame they don't give out t-shirts for their dedicated fans because I would certainly accept one. Anyway, if you want to try them out and don't want to spend any money to start then by all means feel free to use my Digital Ocean referral link to get a $200 credit to use over 60 days. If anything it'll give you plenty of time to try out the rest of my tutorial and if you don't need it past that then so be it. You're not out anything. So with that out of the way let's get into the fun part. I am going to assume, however, for the rest of this that you're using a Digital Ocean Droplet running Ubuntu 20.04 on it. I'm not going to cover installing Linux on a machine at all.

I feel that the combination of a cheap Digital Ocean VM, the power of the dotnet ecosystem and the ease of getting started with Nginx will be enough to convince you to set up more sites and services in this way. Like I said, I've tried countless other setups and this is the one that has been a breeze to use over and over again.

DigitalOcean Referral Badge

1) Configure Nginx Web Hosting

From the Nginx website, "nginx [engine x] is an HTTP and reverse proxy server, a mail proxy server, and a generic TCP/UDP proxy server..." If you've ever used Apache or IIS, then using Nginx should be fairly straightforward. If you've never hosted a site before, then spend some time Googling the three options I've listed. If you follow along everything should "just work" but if you hit a snag it helps to know where to look or how these hosting features work to some extent.

NOTE: I'm going to limit my overall explanations of things in favor of getting you up and running as quickly as possible with minimal conjecture.

Install Nginx

In a new terminal window execute the following commands:

sudo apt udpate
sudo apt install nginx
sudo apt upgrade

Configure UFW Firewall

It's always a good idea to configure a firewall if you're going to open up your server to the world wide web of chaos and mayhem. I guarantee you that as soon as you release your new site to the wild you'll be getting bot traffic and any number of things flooding your log files. So best to restrict this chaos as much as possible.

Again, in your terminal window execute the following (one at a time):

sudo ufw app list
sudo ufw allow 'OpenSSH'
sudo ufw allow 'Nginx Full'
sudo ufw enable

# If Nginx HTTP and/or HTTPS are listed then run the following
sudo ufw delete allow 'Nginx HTTP'
sudo ufw delete allow 'Nginx HTTPS'

# Finish with a status check just make sure it's successful
systemctl status nginx

All this does is set up some new rules for the UFW firewall on Linux, enables SSH and HTTP/S access (ports 22, 80 and 443, respectively) and then checks the status of the Nginx service to make sure it's all good.

Directory Structure and Setup

I typically use the directory naming convention of /opt for my websites and services on Linux. You can place your files in whichever directory you choose, it doesn't make any difference. I know some other common conventions for Linux custom sites and services are /var or /srv.

So, decide on a root directory name and then run the following commands (you'll probably notice I'm a reckless person and am using sudo the whole time... I'll let you decide on making a separate user, I don't care that much. If I destroy my droplet I'll make a new one):

# Make sure you're at the root to get started
cd ~
sudo mkdir /opt/wwwroot/somecooldomain.com
sudo chown -R www-data:www-data /opt/wwwroot/somecooldomain.com
sudo chmod -R 755 /opt/wwwroot/somecooldomain.com

A few notes on the previous commands, as they're relatively important. In every situation I've faced when setting up a new .NET site or service, I always have to use the user and group www-data when running chown. From my understanding, the www-data user and group are the same user and group that owns the Nginx configuration and services. This is why setting the owner of your new service to this user and group allows it to run. I believe this same user is what Apache relies on as well.

The chmod 755 section simply sets the permissions, in a recursive manner with the -R flag on there, for all of the files and folders within your path specification. 755 is quite common and will allow the user and group to do anything with the files or directories and all other users and groups can read and execute but not alter anything. You can read yourself into oblivion as to how many different options there are and what they all mean. A quick Google led me to a nice-looking University of Cambridge page with all of the various details on file operations. You'll probably find this on the web so I'll just say it now, and spare you a Google. But never use 777, unless you have no fear and could care less what happens to your server. Ye been warned.

Server Block Setup

Nginx uses a concept of "blocks" or "directives" that are specified in a configuration file to handle website hosting. Much like any other hosting program we just need to tell Nginx where our site lives, how we want it to handle file names and in which order, and maybe add an SSL and some caching settings to it. We'll change our working directory a bit to be inside the default Nginx install location and get a server block set up.

Following along with the somecooldomain.com example, run the following commands:

sudo nano /etc/nginx/sites-available/somecooldomain.com

Once inside the nano editor (if you use Vim, by all means, use Vim) paste in the following snippet:

server {
    server_name somecooldomain.com;
    root /opt/wwwroot/somecooldomain.com;
    index index.html index.htm;

    location / {
         proxy_pass http://localhost:5000;
         proxy_http_version 1.1;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection keep-alive;
         proxy_set_header Host $host;
         proxy_cache_bypass $http_upgrade;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# Hit Ctrl + X to close and save

Next, we need to create a link between our newly added sites-available entry and our sites-enabled entries. My understanding of this is that we are taking our configuration of an available site, and creating a symbolic link, which then adds a matching record to the sites-enabled directory, which is what Nginx reads from on startup. Please Google that because that's the extent of my knowledge. Run the following to make this magic happen:

sudo ln -s /etc/nginx/sites-available/somecooldomain.com /etc/nginx/sites-enabled/

# Once in the nginx.conf file below uncomment the line that says 
# server_names_hash_bucket_size 64;
sudo nano /etc/nginx/nginx.conf

# Verify the configuration is valid with
sudo nginx -t

# Always a helpful thing to do after this many changes to just reboot
sudo reboot

That's all there is to setting up the Nginx side of things. You've added firewall rules, installed Nginx, registered a new server block (like an Apache virtual host), and created links so that Nginx will recognize your new configuration every time the server starts up. Next, we can move on to an optional step of installing an SSL certificate with Let's Encrypt. Right now we'll only be serving up our .NET application over HTTP. Not a good look in this day and age, so let's go fix that.

2) Install SSL with Let's Encrypt (Optional)

Installing a free SSL certificate with Let's Encrypt on Linux is extremely easy. You can use the Let's Encrypt certbot client. Run the following to obtain a free certificate:

Install Certbot

sudo apt update
sudo apt install certbot
sudo apt install python3-certbot-nginx

Certbot will look for a corresponding server_name directive (which we set up earlier) when you run the following command. So as long as things match up with your selected domain name this should all run without any issues. Run the following commands to generate your new certificate:

sudo certbot --nginx -d somecooldomain.com

Follow all of the prompts and once you're finished it will install your new certificate and automatically update your existing server block with the necessary SSL details. Your new sites-available file should look somewhat like the following (if it's not a perfect match don't worry):

server {
    server_name somecooldomain.com;
    root /opt/wwwroot/somecooldomain.com;
    index index.html index.htm;

    location / {
         proxy_pass http://localhost:5000;
         proxy_http_version 1.1;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection keep-alive;
         proxy_set_header Host $host;
         proxy_cache_bypass $http_upgrade;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/somecooldomain.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/somecooldomain.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

# Certbot redirects
server {
    if ($host = somecooldomain.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    server_name somecooldomain.com;

    listen 80;
    listen [::]:80;

    return 404; # managed by Certbot
}

# Hit Ctrl + X to close and save

Now that we've finished installing our SSL certificate we can verify that our Nginx site configuration is still valid with the following command:

sudo nginx -t

# Once again, let's just reboot to clear it all out and start fresh
sudo reboot

# If you don't want to reboot you could simply reload the Nginx service
sudo nginx -s reload

Automatically Renew Certificates

We'll want to make it easy for ourselves and just have the certificates auto-renew. We can configure a cron job to do this for us with the following:

crontab -e

# Use the following configuration to run it daily
0 12 * * * /usr/bin/certbot renew --quiet

For more information take a look at this blog post from the Nginx website (some inspiration on the cron job came from here): Using Free Let's Encrypt SSL/TLS Certificates with Nginx

3) Install the .NET Core SDK

The next step in the process is to install the .NET Core SDK which includes our necessary runtime. Keep in mind that all of the version numbers in the following steps can be swapped out or mixed and matched however you need. I know that Ubuntu 22.04 is already out there, I'm using this for some applications already, but 20.04 is also in LTS and widely used. So, simply Google for specific version numbers in the following links. Microsoft has a vast amount of information about this entire process, so if you run into any issues please give the Install .NET on Linux documentation a read. I'm sure you'll find what you need somewhere in there. Enter the following in your terminal to get started:

# Add the Microsoft package repository
sudo wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb

# Add the appropriate runtime version or SDK
sudo apt update
sudo apt install dotnet-sdk-6.0

I'm choosing to use the dotnet-sdk-6.0 rather than just the dotnet-runtime-6.0 because I genuinely cannot figure out the difference. I will say that Microsoft takes the cake when it comes to making the naming of things utterly and maddeningly useless and impossible to figure out. Using this option always works for me so if it isn't broken, I'm not going to fix it. Some day it would probably be a good idea for me to test this out, but for now, I'll live with it. Again, please visit the extensive documentation from Microsoft by clicking here.

4) Setting Up a Linux Daemon (Service)

The final step in all of this (apart from building for release and copy-pasting your dlls into your new root directory, which I'm not covering in this) is to register a new daemon (a service in Windows-land, essentially) for our new application. This is what will handle starting and stopping our application. It will help our apps to stop gracefully with cancellation tokens for safe shutdowns and will automatically restart or fire up our application on startup. This part is nice and easy, don't worry. Once again, in your terminal, enter the following commands:

cd ~
cd /etc/systemd/system

# Name the service anything you want but have .service on the end
sudo nano somecooldomain.service

Next, paste in the following text and configure to your project needs:

[Unit]
Description=Some Cool Domain Service

[Service]
WorkingDirectory=/opt/wwwroot/somecooldomain.com
ExecStart=/usr/bin/dotnet /opt/wwwroot/somecooldomain.com/somecooldomain.dll
Restart=always
RestartSec=10
SyslogIdentifier=some-cool-domain-service
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production # Environment appsettings filename
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

[Install]
WantedBy=multi-user.target

Most of the value parts of this file are fairly customizable. The most important part is the WorkingDirectory which tells the service where the executable lives for the dotnet SDK/runtime and then the path to your project's entry point dll file. So, whatever your full path is to your dll should be what you put in there. I kept it simple for the sake of this tutorial. Microsoft, again, has a wonderful amount of documentation on all of this and more in the section: Host ASP.NET Core on Linux with Nginx. Give that a read if you run into any issues and I'm sure you'll get it working correctly. There's a lot of information on customizing and working with the Environment variables, which is great.

Save your new service file and then we'll finish enabling, starting and verifying it with the following:

sudo systemctl enable somecooldomain.service
sudo systemctl start somecooldomain.service
sudo systemctl daemon-reload

# You should see green output after you run this that says "enabled" somewhere
# If you see red then keep reading to learn how to view the logs to see why
sudo systemctl status somecooldomain.service

# Again, reboot and flush out any stuck 1s and 0s (this helps me often)
sudo reboot

If you run into issues you can review your new service logs to troubleshoot. Use any or all of the following to review the logs. They've all worked for me at one point or another:

sudo journalctl -fu somecooldomain.service
sudo journalctl -u somecooldomain.service
cd /var/log/nginx && sudo nano error.log

You can also run your main project dll directly to check for any service errors by running the same path you have in your new service config. This will mimic a manual startup and you can sometimes see immediate program failure output:

# Manually execute your program from the terminal to see errors
/usr/bin/dotnet /opt/wwwroot/somecooldomain.com/somecooldomain.dll

5) Verify Your New Site

If everything went according to plan in the last set of steps then you should be able to make a call to your new site. I don't cover any domain configuration steps, maybe I'll add that in another post, but if you then go to https://somecooldomain.com/some-get-endpoint you should see some response. If you did, then congratulations you've deployed a .NET application to Linux!

Summary

Hopefully this wasn't too difficult to follow, and if you did get stuck at any point I also hope my links to the official documentation helped you out of a bind. I do want to point out that there is a massive section on security and proper management of .NET applications in Linux that can be found in the Microsoft Linux with Nginx documentation. It can be a lot to review and implement, however. Incremental improvement is the name of the game though, right?

Anyway, I hope you learned a thing or two along the way. As always please give a like, or leave a comment below if you liked it, if there's anything I can improve upon or clarify, or if you just want to say hi. Thanks again for reading and happy coding.

What now?

Take a look at some of my other blogs that may be relevant to you after reading this one:

Did you find this article valuable?

Support Charles J by becoming a sponsor. Any amount is appreciated!

ย