Breaking news slowpoke

Ya, rly.

I've missed the beginning of .NET Core, so I've just recently discovered that one can host a .NET Core MVC web-application on a Linux server. I always liked C#/.NET, but I didn't like to be tied to IIS/Windows platform, so that looked very interesting to me.

The whole setup-from-scratch process takes about 15 minutes. I kid you not! And below I will show you it step-by-step (based on this instruction) - how to host a .NET Core MVC application with NGINX and Kestrel on a Linux server.

Publishing

So, we have:

  • Clean, out-of-the-box, remote Linux server running some Linux
  • Some .NET Core MVC application ready for deployment

Connect to your server via SSH:

$ ssh root@YOUR-SERVER-IP

Install .NET Core for Linux on your server and check if it works:

$ dotnet --info
.NET Core SDK (reflecting any global.json):
 Version:   2.2.402
 Commit:    c7f2f96116

Runtime Environment:
 OS Name:     ubuntu
 OS Version:  18.04
 OS Platform: Linux
 RID:         ubuntu.18.04-x64
 Base Path:   /usr/share/dotnet/sdk/2.2.402/

Host (useful for support):
  Version: 2.2.7
  Commit:  b1e29ae826

.NET Core SDKs installed:
  2.2.402 [/usr/share/dotnet/sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.All 2.2.7 [/usr/share/dotnet/shared/Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.2.7 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.2.7 [/usr/share/dotnet/shared/Microsoft.NETCore.App]

On your local machine publish your project by running the following from the project folder:

$ dotnet publish -c Release -o ../_deploy

Create a website folder on the server:

$ mkdir -p /var/www/YOUR-WEBSITE

And copy the contents of ../_deploy from your machine to /var/www/YOUR-WEBSITE/ on the server.

Change the owner of your website's directory so it would belong to NGINX's user (www-data):

$ chown -R www-data:www-data /var/www/

systemd service

Create a systemd config for Kestrel instance:

$ nano /etc/systemd/system/kestrel-YOUR-WEBSITE.service

Edit it like that:

[Unit]
Description=YOUR-WEBSITE

[Service]
WorkingDirectory=/var/www/YOUR-WEBSITE/
ExecStart=/usr/bin/dotnet /var/www/YOUR-WEBSITE/YOUR-WEBSITE.dll
Restart=always
RestartSec=10
SyslogIdentifier=dotnet-YOUR-WEBSITE
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production

[Install]
WantedBy=multi-user.target

Enable and start it:

$ systemctl enable kestrel-YOUR-WEBSITE.service
$ systemctl start kestrel-YOUR-WEBSITE.service

You can check its status:

$ systemctl status kestrel-YOUR-WEBSITE.service

If everything is okay, then it will show something like that:

● kestrel-YOUR-WEBSITE.service - YOUR-WEBSITE
   Loaded: loaded (/etc/systemd/system/kestrel-YOUR-WEBSITE.service; enabled; vendor preset: enabled)
   Active: active (running) since Thu 2017-08-10 11:30:09 UTC; 1s ago
 Main PID: 15628 (dotnet)
    Tasks: 14
   Memory: 25.4M
      CPU: 1.380s
   CGroup: /system.slice/kestrel-YOUR-WEBSITE.service
           └─15628 /usr/bin/dotnet /var/www/YOUR-WEBSITE/YOUR-WEBSITE.dll

NGINX

Install NGINX:

$ apt-get install nginx

Now, there are 2 options how NGINX can send requests to Kestrel: via TCP or via Unix socket.

TCP

Your Program.cs:

// ...

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseUrls("http://localhost:5000/")
        .UseStartup<Startup>();

// ...

NGINX config (/etc/nginx/sites-available/default):

# ...

server {
    listen 80;
    listen [::]:80;

    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;
    }
}

# ...

Unix socket

This should be better in terms of performance as there is no TCP overhead. Even if there was no performance impact, on a local machine “talking” through Unix socket simply make more sense than communicating over TCP.

Your Program.cs:

// ...

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseLibuv()
        .ConfigureKestrel(
            (context, serverOptions) =>
            {
                // /var/www/YOUR-WEBSITE
                string root = Path.GetDirectoryName(
                    System.Reflection.Assembly.GetExecutingAssembly().Location
                );
                // if we are (behind NGINX) and on Linux, then can use sockets
                if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
                {
                    // path to socket has to be absolute
                    string socket = Path.Combine(root, "kestrel.sock");
                    serverOptions.ListenUnixSocket(socket);
                }
                else
                {
                    serverOptions.Listen(IPAddress.Loopback, 5000);
                }
            }
        )
        .UseStartup<Startup>();

// ...    

For that to work you need to add Transport.Libuv package, so you'll have the following in your .csproj:

<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" Version="2.2.0" />

NGINX config in this case:

# ...

location / {
    proxy_pass http://unix:/var/www/YOUR-WEBSITE/kestrel.sock;
    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;
}

# ...

Save it and restart NGINX:

$ systemctl restart nginx.service

Now open your web-browser and go to http://YOUR-SERVER-IP/. It fucking works!