ASP.NET Core + AWS + RHEL+ Kestrel

Deploying to the cloud using AWS/RHEL/Kestrel (part 2 of 2)

Part 2: Setting up the RHEL production server using Kestrel

This tutorial provides a way to deploy ASP.NET Core apps to bare metal without containers or reverse proxies. We’ll use an Amazon EC2 instance running RHEL and deploy Kestrel using non-sudo privileges and auto-restart capabilities given by the systemd portion of the Linux kernel. Overview:

Image credit: Microsoft

It’s funny I found this picture on the MS site but there’s no docs on how to go about it, nor did a google query return what I wanted, so I had to dig deep and use my own brain and life experiences to develop this. If you already have Centos/RHEL server, skip the first AWS part.

Get an AWS account, setup a zone close to your house, go to the EC2 dashboard, and click the blue “Launch Instance” button:

click Launch Instance

Pick RedHat

RHEL, the best Linux flavor out there

Click on the Configure Security Group Tab

Keep defaults and click on 6

Add rules to support port 80 and 443 for internet traffic, open ports if using a different firewall than default AWS at this time. Use a new security group.

Click Launch, make some SSH keys:

Put the keys where they belong, mine was called exampleProject.pem:

mv Downloads/exampleProject.pem ~/.ssh/.

Make an elasticIP from command line:

aws ec2 allocate-address --domain "vpc" --region us-east-1

Click on the Elastic IP section of the EC2 dashboard and associate address.

Give the SSH key proper permissions:

chmod 400 ~/.ssh/exampleProject.pem

On the EC2 dashboard, click Running Instances, then click Connect

We stored our keys in the .ssh directory of the user. Use that same example syntax, but add the directory structure for the ssh key:

ssh -I "~/.ssh/exampleProject.pem"

Now we’re inside a Linux server, get the SDK’s repo:

sudo rpm -Uvh

Update the server: sudo yum update

Install the database, dotnet, and git

sudo yum install dotnet-sdk-2.1 git postgresql-server postgresql-contrib

Setup the database with a resource user:

sudo postgresql-setup initdb
sudo systemctl start postgresql
sudo systemctl enable postgresql
sudo su postgres
CREATE DATABASE example_project;
CREATE USER example_project_user;
ALTER USER example_project_user WITH PASSWORD 'SuperSecret123';
ctrl-D twice to exit

Lockdown the database with md5:

sudo vi /var/lib/pgsql/data/pg_hba.conf

Go to the bottom G , change peer and ident to md5

Restart database sudo systemctl restart postgresql

Allow dotnet to bind to lower ports:

sudo setcap ‘cap_net_bind_service=+ep’ /usr/share/dotnet/dotnet

Get mono and Nuget:

sudo su -c ‘curl | tee /etc/yum.repos.d/mono-centos7-stable.repo’
sudo yum install mono-devel
sudo curl -o /usr/share/dotnet/nuget.exe
sudo su dotnetuser
vi ~/.bashrc , i , copy/paste bold text below, :wq
alias nuget=”mono /usr/share/dotnet/nuget.exe”
source ~/.bashrc

Sanity check: type nuget last line of response should be

For more information, visit

After confirming our package manager works, we are ready to let EF-core do its thing, migrate the DB: dotnet ef database update

Migration script created in part 1, we run database update so that the production database matches the dev

Add the resource user with password

sudo useradd dotnetuser and then sudo passwd dotnetuser

Sign into new user: su — dotnetuser. Push/Pull project with GitHub.

git clone

Buy a cert somewhere reputable, but don’t spend more than $10. Use openssl to sign the cert with the key, you end up with some binary, don’t forgot your password, for the example I’ll use SuperSecret123

sudo openssl pkcs12 -export -out seniordevops.pfx.txt -inkey seniordevops.key -in seniordevops_com.crt

My encryption was on another Linux machine. I renamed it txt so that it would allow me to transfer, then named it back to pfx after SCP-ing.

mkdir .ssh
cd .ssh
scp rdines38@ ./
mv seniordevops.pfx.txt seniordevops.pfx

Change the Program.cs to use this method in production:

using System.Net;
using Microsoft.AspNetCore.Server.Kestrel.Core;
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
return WebHost.CreateDefaultBuilder(args)
.UseKestrel(options =>
options.Limits.MaxConcurrentConnections = 100;
options.Limits.MaxConcurrentUpgradedConnections = 100;
options.Limits.MaxRequestBodySize = 10 * 1024;
options.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Limits.MinResponseDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Listen(IPAddress.Any, 80);
options.Listen(IPAddress.Any, 443, listenOptions => {listenOptions.UseHttps("/home/dotnetuser/.ssh/seniordevops.pfx", "SuperSecret123");

Go into project directory, publish:

dotnet publish --configuration Release

Check deployment, run dotnet against the published dll:

dotnet /home/dotnetuser/example/bin/Release/netcoreapp2.1/ExampleProject.dll

Find your IP to the server, put it in your webbrowser, if your DNS isn’t setup, you’ll have to accept the security risk of a non-verified https site. Conversely, setup an A record and call your site by its real name, its rewarding.

Dotnet running in server foreground, almost there

Cool, looks good. Now let’s tell the Linux kernel to serve it up in the background 24/7. Exit the program and the resource user ctrl-c, ctrl-d. Make a system daemon like this: sudo vi /etc/systemd/system/dotnetcore.service and add the following:

Description=Dot-Net-Core service
ExecStart=/usr/bin/dotnet /home/dotnetuser/example/bin/Release/netcoreapp2.1/ExampleProject.dll

Start it, make rebootable, and verify. sudo systemctl start dotnetcore, sudo systemctl enable dotnetcore, and systemctl status dotnetcore


Verify that it works in your browser too. This is just a tiny portion of what it takes to make a production server, yet a key task has been accomplished: daemonizing dotnet to run without elevated privileges. Feel free to reach out to me for questions here.