Click here to Skip to main content
13,898,214 members
Click here to Skip to main content
Add your own
alternative version

Stats

6.8K views
26 bookmarked
Posted 6 Jan 2019
Licenced CPOL

.NET Core nginx, and Postgres with EF on an rPi

, 11 Jan 2019
Rate this:
Please Sign up or sign in to vote.
Implementing an SSL capable server in .NET Core WITHOUT ASP.NET, using nginx, and testing Postgres with EF, all running on an rPi.

Contents

Introduction

This year I've decided to focus on IoT, mainly the Raspberry PI (rPi) and Beaglebone Single Board Computers (SBC's).  I'll call it my "personal year of IoT."  My interest is initially taking me down the path of testing out whether one of these devices can be used as a simple web server (yes, of course it can) but has enough horsepower to support a SQL database and a "real" web application.  As a test case, I'm planning on using a website that I developed for a client a few years back, but for the purposes of this article, I just want to vet a variety of technologies.  At the end of the day (not this article-day), I should be able to determine the viability of using a very low cost SBC as a decent web server for low-bandwidth needs.  Along the way I may play with some of the hardware features as well.

For my primary mission, I'm planning on using PostgresSQL (I have a personal historic distaste for MySQL) and DotNet Core for the web server.  Three initial challenges present themselves:

  1. Importing a SQL Server database into Postgres (a Docker image of SQL Server is not viable is requires 2GB of RAM and the rPi only has 1GB RAM and the bigger issue is that it supports only x64 processors, not ARM processors.)  There's an Entity Framework Core provider for Postgres here
  2. Coercing .NET Core into using my server code, as the website that I wrote for my client does not use ASP.NET.
  3. Figuring out how SSL certificates work in the world of Linux with .NET Core.
  4. Step 3 involves configuring a reverse proxy server, which I demonstrate using nginx.

So there will be some challenges not related to SBC's, and some definitely related!

Getting Started with the rPi

The first step is to assemble the KanaKit rPi and get it fired up.  After that, I want be able to boot the OS from a USB drive rather than use the micro SD card.

Unpacking, Assembling, and Loading an OS

Opening the big box, we find inside the rPi itself, power supply, HDMI cable, SD card, case, and heat sinks:

Unpacking those boxes, we get to the actual hardware!

Assembling the rPi in the case took a good 10 or 15 minutes of struggling with the case (I refused to watch the video!)  Turns out you have to sort of slide the rPI into position so that the SD card slot is properly positioned in the access hole for the case.  Finally it was assembled (including heat sinks):

The SD card comes installed with NOOBS, "New Out Of the Box Software" which lets you choose the OS you wish to install:

When I tried installing the recommended Raspbian OS (the first checkbox) the installation process hung (I did wait several minutes.)  With trepidation I pulled the power and restarted the rPi, which happily booted back into NOOBS.  Selecting the second option, Raspbian Full, installed without any problems.  Great!

Checking out the Hardware and Capabilities

One of the nice things about this version of the rPi is the built in WiFi, which was very easy to configure.  I've never been able to get WiFi working on the BeagleBone Black, and WiFi was a requirement as I want to be able to work on this project without being confined to my office where I have cable connectivity (my office is cluttered enough as it is with BeagleBone projects for another client.)

What Version rPi?

The KanaKit (at least as far as I could tell) did not actually tell me what version rPi I had, so after some digging, I found this site that lists the different versions.  Opening a command line window and typing:

cat /proc/cpuinfo

I determined that the "version" reported is "a020d3" and looking that up on the site (link above) I verified that I have an rPi 3B+:

Does it Support Boot from USB?

After much googling, I discovered that the 3A+ and 3B+ rPi's can boot from USB without any configuration changes.  While the configuration changes looked simple, I decided to verify, based on those instructions, that the rPi was USB boot capable.  Turns out that if you type in:

vcgencmd otp_dump | grep 17:

and you get back this code: 0x3020000a, it means that the OTP (One Time Programmable) bit has been programmed to boot from USB.  This bit says to try and boot from the SD card and if that doesn't work, scan the USB devices for bootcode.  Apparently, given that this is "one time programmable", you can still change to boot from the SD card with a little finagling.

Imaging an OS onto a USB Drive

The next step was to image the OS onto a USB drive.  I already had a 2TB spinny (not SSD!) drive lying around, and given that the rPi doesn't support USB 3, the performance hit of using a mechanical drive vs. SSD seemed irrelevant, at least at this stage of the game -- and besides, I didn't have any unused SSD drives and I wanted to move along with the project.

  1. From the rPi download site, I selected "Raspbian Stretch with desktop and recommended software" and downloaded it.
  2. From the 7Zip site, I downloaded and installed 7Zip and unzipped the file from step 1 so I now had the "2018-11-13-raspbian-stretch-full.img" file.
  3. I then downloaded and installed Etcher, which is a simple three step process for imaging the drive with the .img file

Etcher UI:

After imaging the drive and removing the SD card, the rPI booted from the USB drive!

Mandarin oranges, scented candles (behind the oranges) and specialty chocolate (the bag behind the oranges) are only required if other incantations are required to get things working.

Graceful Shutdown

As much as possible, always gracefully shutdown your rPi with:

sudo shutdown -h now

Rebooting the rPi

On the command line or terminal window, type:

sudo reboot

Next Steps - PuTTY and WinSCP

Ultimately I don't want to have to hook up a monitor, keyboard, and mouse to the rPi (and therefore it also doesn't need a desktop UI).  Instead, work with the rPi will be done using PuTTY (telnet client) and WinSCP for file transfers.

  1. Download and install PuTTY from here.
  2. Download and install WinSCP from here.

Enable SSH

The Debian OS has an SSH server built into it, however we need to enable SSH.  One way to enable SSH is to enter:

sudo raspi-config

which brings up a simple UI:

  1. Navigate to item 5 "Interface Options"
  2. Select "P2 SSH - Enable/Disable remote command line accees to your Pi using SSH"
  3. Select "Yes" when prompted "Would you like the SSH server to be enabled?"
  4. Select "OK"
  5. Select "Finish" to exit the config app.

Read more about the raspi-config app here.

Now we can telnet directly into the rPi once we know the IP address, which is determined by using:

ifconfig

in the console window.  Since I'm using WiFi, I find the IP address in the wlan0 section as inet 192.168.0.15

Other options to determine the IP address include using hostname -I which gives you the network IP, the local IP, and the IPv6 addresses.

PuTTY

Fire up PuTTY and enter the IP address of your rPi:

I find it useful to save the IP address.

The first time you connect (or whenever the rPi's IP address changes) you'll see a dialog like this:

Click on "Yes" and proceed to login:

The username is "pi" (unless you changed it) and the password is whatever you used when setting up the OS the first time it booted.

If you want to automatically log in, you can either use (change the IP address for your rPi of course!) : putty pi@192.168.0.15 -pw [your password] on the command line or create a desktop shortcut.  As an exercise for the reader (mwahaha), a better approach would be to use key pairs.

WinSCP

Setup for WinSCP (SCP stands for Secure Copy Protocol) is essentially the same except that here you can enter in the username and password with the option to save both:

When you save the session as a site, you have the option to save your password as well:

Once you've logged in, you'll see on the left the Windows directory structure and on the right, the rPi's OS directory structure.  This makes it really easy to copy and paste files between the two and even edit files with a simple text editor.

WinSCP, on the first time connecting, will also display a security alert:

Select "Yes".

First Big Step: Installing PostgresSQL

The first big step is to install PostgresSQL.  Happily there is a great article to tell me how to do this, as otherwise I'd never be able to figure anything out.  In the PuTTY terminal (or from a terminal window on your rPi itself), type in:

sudo apt install postgresql libpq-dev postgresql-client postgresql-client-common -y

Hint: If you're using PuTTY, right-click the mouse to copy text from the clipboard into the terminal window.  If you want to copy text from the terminal window to the clipboard, select the text with your mouse and left-click.

At this point, just follow the rest of the steps up to, but no including, "Now connect to Postgres using the shell and create a test database."  The steps I performed are:

sudo su postgres
createuser pi -P --interactive

and respond the prompts as indicated in the article or the screenshot of what I did (obviously, enter your own password):

Hint: once logged in as a superuser ("su"), you can type exit to, well, exit as that user and return to the "pi" user.  Don't do this yet though if you're following the steps in this section.

Connecting to Postgres Remotely

We all want to work remotely, right?  I mean, in this day and age, with all this tech, driving in the office seems absurd.  Sorry, off topic.  Continuing further down the tutorial, let's enable Postgres so we can connect to it remotely.  As per the article I linked to above:

1. Edit the PostgreSQL config file /etc/postgresql/9.6/main/postgresql.conf to uncomment the listen_addresses line and change its value from localhost to *.

Using nano, a simple terminal editor, edit the line as indicated in step 1.  We're using nano as the superuser so we have the permissions to save the file.

postgres@raspberrypi:/home/pi$ nano /etc/postgresql/9.6/main/postgresql.conf

Hint: The current path is on the left of the $, what you type in is on the right of the $.

Don't forget to remove the # at the start of the line!

2. Edit the /etc/postgresql/9.6/main/pg_hba config file and change 127.0.0.1/32 to 0.0.0.0/0 for IPv4 and ::1/128 to ::/0 for IPv6.

postgres@raspberrypi:/home/pi$ nano /etc/postgresql/9.6/main/pg_hba.conf

Make sure you edit the lines that are NOT commented out!

Regarding the third step, restarting the postgres service, I was unable to get this step to work -- it kept asking for the postgres user's password, and there isn't one.  After rebooting the rPi, I tried the command again and it said the service hadn't started!  So I don't know what's going.

Installing pgadmin on Your Windows Box

Download pgadmin 3 (very important that you download the no longer supported pgadmin 3, as it is a desktop app, not a localhost web app, yuck) from here (or pgadmin 4 from here)and install on your Windows box.  Certainly you can use pgadmin4, but I personnally prefer to have an application that doesn't require being run in my browser.  Regardless, the UI's are similar.  Using pgadmin3, create a connection to Postgres on your rPi (change the IP address and password accordingly):

If you modified the files above correctly (I didn't the first time), you should be able to connect and explore the database:

Unfortunately, I'm finding pgadmin 3 to be buggy (it's reporting query errors when I try to add tables, etc., but it doesn't seem to prevent the operation from completing) but pgadmin 4 (the browser version) works a lot better.  Here's a screenshot from pgadmin 4:

Note that when I first ran pgadmin 4, it couldn't start the "pgadmin 4 server."  When I tried it a second time, it worked.  Go figure.  The point being, we can connect to the Postgres server running on the rPi!

How's our rPi Doing?

Let's use free -h to see how the memory allocation looks:

The 32M of shared memory is for the display driver.  Given that there's 521M free, we're doing pretty well after installing Postgres.

And df -h to see how our disk free space looks:

Hint: the "-h" tells both commands to shorten the amount displayed to the byte, K (kilobyte), M (megabyte), G (gigabyte), or T (terabyte) with 1 digit of precision (past the decimal point) which makes for a much more readable display.

Installing DotNet Core

Again, "I know nothing!" and rely on others to tell how to do things.  "Dave the Engineer" has a great MSDN blog post on setting up the .NET Core runtime on the rPi.  But this is for .NET Core 2.0, and I'd like to install the latest stable version, which is 2.2 (I'm tempted to install 3.0, but I'll wait.)  More googling finds Scott Hanselman's post on installing .NET Core 2.1.  Skip all the Docker stuff and go down to the bottom of the post where you find the section "Second."  I particularly don't want to use Docker as it's an added complexity and as Scott writes, I want to use .NET Core "on the metal."  Interestingly, Scott's article includes installing the SDK, which from all I've read is unsupported.  Dejan Stojanovic has an article for installing 2.2!  If you think space junk in orbit is a problem, it's becoming a real nuisance to find posts on the most current technologies on the Internet!

Skipping down the middle of the article, we're supposed to run this:

wget https://download.visualstudio.microsoft.com/download/pr/36bff52b-2cdd-4011-8e92-d00f7537704f/9885ba267b1ef29a1401adc387d9a016/dotnet-sdk-2.2.101-linux-arm.tar.gz

Good grief.  How do you even figure out all these crazy numbers?  Amazingly, that worked:

Following his post, we now run:

sudo mkdir -p /bin/dotnet && sudo tar zxf dotnet-sdk-2.2.101-linux-arm.tar.gz -C /bin/dotnet
export DOTNET_ROOT=/bin/dotnet 
export PATH=$PATH:/bin/dotnet

and edit the bash.rc file:

sudo nano ~/.bashrc

and add this line at the end:

export PATH=$PATH:/bin/dotnet

Wow, it worked!  (I'm always suprised when all this stuff works in Linux.)

Testing a .NET Core Application

Following the instructions here:

  1. In a command line window on your PC (not the rPi) create a directory for the test app and cd into that directory.
  2. Type in: dotnet new console
  3. Type in: dotnet publish -r linux-arm

Then:

  1. Drill into the folder structure to find the publish folder - using WinSCP, copy everything in this folder into a folder on your rPi.
  2. In the PuTTY window, cd into the folder created on the rPi and type in: chmod 755 ./consoleApp to make that file an executable.
  3. Type: ./consoleApp

If all went well, you should see Hello World emitted:

And there was much rejoicing!!!

Connecting to Postgres with C# and .NET Core on the rPi

Now let's see if we can connect to the Postgres database with an app running on the rPi.  We should be able to test the code in Visual Studio, connecting to Postgres on the rPi -- hah, there's a secondary application, using the rPi as a database server!

Create A Database for Testing

Before we get started with the C# side of things, let's first create a database and a simple table for testing.  In the pgadmin SQL window, execute this:

CREATE TABLE public."TestTable"
(
  "ID" serial primary key NOT NULL,
  "FirstName" text COLLATE pg_catalog."default",
  "LastName" text COLLATE pg_catalog."default"
)
WITH (
  OIDS = FALSE
)
TABLESPACE pg_default;

ALTER TABLE public."TestTable" OWNER to pi;

Hint: in pdgadmin 3, select Tools -> Query Tool, in pdgadmin 3, click on the SQL icon.

Hint: The serial (or bigserial) keyword is Postgres' way of specifying an auto-increment field.

Add Nuget Packages

Using the "consoleApp" project created above, right-click on the Dependencies:

and add two dependencies:

  1. Microsoft.EntityFrameworkCore
  2. Npgsql.EntityFrameworkCore.PostgreSQL

You're project should now reference these in the NuGet sub-folder:

Additional Namespaces

Add the following namespaces:

using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics;

using Microsoft.EntityFrameworkCore;

Diagnostics will be used to time the database operations.

The TestTable Model and DbContext

public class TestTable
{
  [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
  public int ID { get; set; }

  public string FirstName { get; set; }
  public string LastName { get; set; }
}

public class Context : DbContext
{
  public DbSet<TestTable> TestTable { get; set; }

  public Context(DbContextOptions options) : base(options) { }
}

Main

Main is modified to execute TestPostgres which we'll see next.

static void Main(string[] args)
{
  Console.WriteLine("Hello World!");

  TestPostgres();
  Console.ReadLine();
}

TestPostgres Method

This is an async void method, normally to be avoided!  This method will automatically insert a couple rows if no data is found and time the operations.  In the code below, replace [your password] with the password you used when creating the Postgres pi user.

static async void TestPostgres()
{
  var contextBuilder = new DbContextOptionsBuilder();
  // Database name is case-sensitive
  contextBuilder.UseNpgsql("Host=192.168.0.15;Database=Test;Username=pi;Password=[your password]");

  Stopwatch stopwatch = new Stopwatch();
  stopwatch.Start();

  using (var context = new Context(contextBuilder.Options))
  {
    Console.WriteLine(stopwatch.ElapsedMilliseconds + "ms");
    stopwatch.Restart();

    var items = await context.TestTable.ToListAsync();
    Console.WriteLine("First query: " + stopwatch.ElapsedMilliseconds + "ms");
    stopwatch.Restart();

    Console.WriteLine("Number of items: " + items.Count);
    
    if (items.Count == 0)
    {
      TestTable t1 = new TestTable() { FirstName = "Marc", LastName = "Clifton" };
      TestTable t2 = new TestTable() { FirstName = "Kelli", LastName = "Wagers" };
      context.Add(t1);
      context.Add(t2);
      context.SaveChanges();
      Console.WriteLine("Insert: " + stopwatch.ElapsedMilliseconds + "ms");
      Console.WriteLine("t1 ID = " + t1.ID);
      Console.WriteLine("t2 ID = " + t2.ID);
      stopwatch.Restart();
    }
    else
    {
      items.ForEach(t => Console.WriteLine("ID: " + t.ID + " FirstName: " + t.FirstName + " LastName " + t.LastName));
    }

    // Query again to see how long a second query takes.
    var items2 = await context.TestTable.ToListAsync();
    Console.WriteLine("Second query: " + stopwatch.ElapsedMilliseconds + "ms");
    stopwatch.Restart();
  }

  using (var context = new Context(contextBuilder.Options))
  {
    // Query again to see how long a second query takes.
    var items2 = await context.TestTable.ToListAsync();
    Console.WriteLine("Third query: " + stopwatch.ElapsedMilliseconds + "ms");
  }
}

Run the Test in Visual Studio

Run the program, and you should see something similar to this:

Notice the first query, which establishes a connection to the database, takes almost 1.5 seconds.  I've seen this as high as 4 seconds.

Run the Test on the rPi

Don't forget to run the publish command again:

dotnet publish -r linux-arm

Using WinSCP as before, copy everything over to the rPi. 

Hint: Normally we don't need to copy everything, only the most recent changes, but since we added a couple NuGet packages, it's best to copy the whole kit and caboodle as there will probably be dll's and so's that timestamped by their release date and won't show up if you sort the folder contents by descending date.

Hint: Interestingly, once consoleApp has been chmod'd to be an executable, we don't have to do this again.

After deleting the test data created in the run from Visual Studio, you should see something similar to this:

Hint: Press the Enter key to exit the program, as it's sitting on the Console.ReadLine() call.

Notice the rPi takes a whopping 8 seconds to create the connection to Postgres.  Fortunately, once the connection has been established, the queries run in under 100ms, but insert took over a second.

Running the test program again, we get our data back instead:

Performance Improvement?

I played around a little with forcing turbo mode (which voids your rPi warranty!) as described in this writeup but found no performance improvement.  I didn't try the overclocking options as those may make the rPi unbootable and I didn't want to fuss with recovering the /boot/config.txt file, which is where you set the configuration options.

Bare Bones Server

There already is an excellent server on GitHub here and it already supports HTTPS, so why reinvent the wheel?  Because my curiosity takes me to strange corners of programming, and I want to see (and learn) how one can use .NET Core to write a server that is not based on the ASP.NET Core stack.  Having written a simple service using the ASP.NET Core stack, I found it easy to use, the dependency injection (DI) is cool, the automatic extraction of parameters from the HTTP headers, JSON POST body, etc., are all really snazzy.  Definitely recommended!  But still, I want try out a bare metal implementation for no other reason than it's what I like to do.  So if you're curious like me, read on.

HTTP Server

An HTTP server is trivial.  First, let's modify Main to start up an HttpListener:

static void Main(string[] args)
{
  Console.WriteLine("Hello World!");

  // TestPostgres();
  StartServer();
  Console.WriteLine("Press ENTER to exit.");
  Console.ReadLine();
}

We'll start the server and respond with the GET path as well as write the verb and path to the console (remember your rPi's IP may be different):

static void StartServer()
{
  HttpListener listener = new HttpListener();
  listener.Prefixes.Add("http://192.168.0.15:5000/");
  listener.Start();
  Task.Run(() => WaitForConnection(listener));
}

static void WaitForConnection(object objListener)
{
  HttpListener listener = (HttpListener)objListener;

  while (true)
  {
    HttpListenerContext context = listener.GetContext();
    string verb = context.Request.HttpMethod;
    string path = context.Request.RawUrl.LeftOf("?").RightOf("/");
    Console.WriteLine($"Verb: {verb} Path: {path}");

    byte[] buffer = Encoding.UTF8.GetBytes(path);

    context.Response.StatusCode = (int)HttpStatusCode.OK;
    context.Response.ContentLength64 = buffer.Length;
    context.Response.OutputStream.Write(buffer, 0, buffer.Length);
    context.Response.Close();
  }
}

Add at the top of the file:

using System.Net;
using System.Text;

And for completeness, the two extension methods that I use everywhere:

public static class ExtensionMethods
{
  public static string LeftOf(this String src, string s)
  {
    string ret = src;
    int idx = src.IndexOf(s);

    if (idx != -1)
    {
      ret = src.Substring(0, idx);
    }

    return ret;
  }

  public static string RightOf(this String src, string s)
  {
    string ret = String.Empty;
    int idx = src.IndexOf(s);
    
    if (idx != -1)
    {
      ret = src.Substring(idx + s.Length);
    }

    return ret;
  }
}

Publish, WinSCP, and test:

Great!  From there, the unsecure world of HTTP is our playground!

Under the Covers - htop

It's interesting to use htop, an interactive process viewer, that comes with Debian to see how many processes are started by .NET Core running the web server application (there are 10 ./consoleApp processes):

nginx

Implementing an HTTPS server however requires a reverse proxy server such as nginx or Apache which will take the incoming IP and route it to a specified localhost port and return responses back to the client making the request.  Before diving into HTTPS and SSL certificates, let's get a basic reverse proxy working with our C# code.

Install and Test nginx

To install nginx on the rPi, enter this in a terminal window (this I learned from the rPi site):

sudo apt-get install nginx

Start the server with:

sudo /etc/init.d/nginx start

Now navigate to you rPi's IP address and you should see the nginx welcome screen:

How is This Site Served?

Since I know nothing about nginx, I'm going to explore some of the basics.  Obviously nginx created a default website and webpage somewhere.  The configuration file in /etc/nginx/sites-available, where we see a file called default:

If we inspect this file using WinSCP (or if you're in the terminal, use nano /etc/nginx/sites-available/default or cat /etc/nginx/sites-available/default), we see some important configuration lines (the following is truncated from the actual file):

listen 80 default_server;
listen [::]:80 default_server;

root /var/www/html;

index index.html index.htm index.nginx-debian.html;

location / {
  # First attempt to serve request as file, then
  # as directory, then fall back to displaying a 404.
  try_files $uri $uri/ =404;
}

Great, so it's serving pages from /var/www/html, where we see the index file:

So now we know where the default website is serving up static content.

Configuring a Reverse Proxy to Run the .NET Core ConsoleApp

Next, I want to route anything that isn't static content to my server.  Reading the Beginner's Guide on nginx, I'll change the try_files command to proxy_pass and specify the URL that the consoleApp program is listening on.  This has to be done as a superuser, so we use this command line: sudo nano /etc/nginx/sites-available/default to edit the file, commenting out the try_files command and adding proxy_pass http://127.0.0.1:5000/;

and after saving the file, restarting nginx with:

sudo /etc/init.d/nginx reload

Hint: If you get an error reloading nginx, go back into the editor and undo your edits, save the file, and see if reload now works.  Then try changing the file again.  I don't know what I did, but the first time I made the file change above, I got an error (didn't bother to follow the instructions to see what the error was), so I reverted my changes and re-applied them, and suddenly no error!

Hint: using http://localhost:5000/ does not work - you get a "Not Found" error from nginx, you must use <a href="http://127.0.0.1:5000/">http://127.0.0.1:5000/</a>.

Lastly, change the IP address that the consoleApp server is listening to, to 127.0.0.1:

listener.Prefixes.Add("http://127.0.0.1:5000/");

Publish, WinSCP the changed files over, start the consoleApp, and when you try a URL as depicted in the screenshot, you should get:

and in the terminal window:

Hint: I got a few errors before getting this right, and a very helpful Digital Ocean page led me to this command:

sudo tail -30 /var/log/nginx/error.log

which displays the last 30 lines of errors reported by nginx.  Very useful in figuring out what's going wrong!

Hint: auto-start nginx (from here):

Auto-start NGINX at boot:

cd /etc/init.d
sudo update-rc.d nginx defaults

Remove auto-start:

sudo update-rc.d -f nginx remove

HTTPS

The great thing about a reverse proxy is that it handles HTTPS for you -- your local server can be HTTP.  Also, nginx supports Server Name Identification (SNI) which means that I can run several domains, each with different SSL certificates, from the same physical device and IP address.  For now, we'll just create a self-signed certificate for testing.  First, let's prove that HTTPS isn't working:

Yup, not working.  Next, let's create a self-signing certificate following (yes, once again I revert to people that know a lot more than me) Karlo van Wyk's instructions here.  Basically, forgive me for essentially plagiarizing his instructions, create a folder and in that folder, create a file called localhost.conf.  You can use the nano editor, and for some reason the instructions say to launch nano as the superuser via sudo nano, I'm not sure why.  Next, copy the configuration file he provides (I feel really bad posting it here again, but for completeness of this article, it seems necessary):

[req]
default_bits = 2048
default_keyfile = localhost.key
distinguished_name = req_distinguished_name
req_extensions = req_ext
x509_extensions = v3_ca

[req_distinguished_name]
countryName = Country Name (2 letter code)
countryName_default = US
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = New York
localityName = Locality Name (eg, city)
localityName_default = Rochester
organizationName = Organization Name (eg, company)
organizationName_default = localhost
organizationalUnitName = organizationalunit
organizationalUnitName_default = Development
commonName = Common Name (e.g. server FQDN or YOUR name)
commonName_default = localhost
commonName_max = 64

[req_ext]
subjectAltName = @alt_names

[v3_ca]
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
DNS.2 = 127.0.0.1

Feel free to edit your locality information.

Next, create the certificate key pairs (public and private):

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout localhost.key -out localhost.crt -config localhost.conf

Then copy them to the /etc/ssl folders as the superuser (so you have write permissions):

sudo cp localhost.crt /etc/ssl/certs/localhost.crt
sudo cp localhost.key /etc/ssl/private/localhost.key

Lastly, using sudo nano, edit the default file like we did above:

sudo nano /etc/nginx/sites-available/default

uncommenting the SSL configuration lines and adding a couple extra lines, so it looks like this:

listen 443 ssl default_server;
listen [::]:443 ssl default_server;
ssl_certificate /etc/ssl/certs/localhost.crt;
ssl_certificate_key /etc/ssl/private/localhost.key;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;

Finally, reload the nginx configuration as we did earlier:

sudo /etc/init.d/nginx reload

Then browse to the rPi's IP using HTTPS, and you should get this:

Clearly the connection is now being made and Chrome is alerting you that the certificate in not valid, which is obvious because we're creating a test certificate rather than one with a certificate authority such as LetsEncrypt.  So, click on "Advanced" and "proceed...." to get to the site:

And there you go!

Running the Server as a Service

Ideally, the web server should start up automatically when the rPi boots -- implementing this is straight forward though I did have to read through both this link on hosting ASP.NET Core and this link on setting up a simple console app as a service.  The "trick" here is not to perform a Console.ReadLine as this times out the startup process, and obviously not to exit the application!  Following the guidance from second link, add these namespaces:

using System.IO;
using System.Reflection;

and edit Program.cs:

static readonly CancellationTokenSource tokenSource = new CancellationTokenSource();

static void Main(string[] args)
{
  StartService();
}

and the implementation (trimmed down from the example in the second link):

static void StartService()
{
  AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
  StartServer();

  while (!tokenSource.Token.IsCancellationRequested)
  {
    Thread.Sleep(1000);
  }
}

private static void CurrentDomain_ProcessExit(object sender, EventArgs e)
{
  tokenSource.Cancel();
}

Hint: The while loop can probably be improved with a task that waits until the token is cancelled.

Next, create a .service file in the /lib/systemd/system folder that looks like this (use sudo nano to create and edit the file):

[Unit]
Description=Web Server
After=nginx.service

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/webserver
ExecStart=/home/pi/webserver/consoleApp
Restart=always

[Install]
WantedBy=multi-user.target

As you can see, I place consoleApp (and its files) into a folder called webserver and I also called the service file webserver.service.

Once service file is created, create a symlink for easy reference:

sudo systemctl enable webserver

You can manually start the server right away with:

sudo systemctl start webserver

and check on its status with:

systemctl status webserver.service

Hint: Any console output is logged and displayed when you view the service status!

You should see something similar to this:

We can also see the service running using htop:

I found that errors are fairly indicative of the the problem.

Revisions

1/11/2019: Added setting up the web server as a service.

Conclusion

This article accomplishes quite a few things:

  1. Determining the rPi version and capabilities.
  2. Imaging an OS onto a USB drive.
  3. Setting up SSH so we can use PuTTy and WinSCP to communicate to the rPi.
  4. Installing Postgres and creating a test database.
  5. Installing .NET Core 2.2 and testing out Postgres connectivity, both from a Windows box and directly on the rPi.
  6. Creating an "echo" HTTP server.
  7. Installing and configuring nginx for HTTP.
  8. Configuring nginx as a reverse proxy to our .NET Core "echo" server.
  9. Configuring nginx with a test certificate.

And the big accomplishment here is that we did all this without using ASP.NET Core.  Frankly, it's damn hard to find any articles that are not related to ASP.NET Core with regards to nginx, setting up HTTPS, etc., so hopefully the reader will appreciate the bare-metal approach that I've taken here.  My next article will dive more into creating a real website (a port from an existing website), working with performance issues (that horrid 8 second connect delay to Postgres) and who knows what else.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Marc Clifton
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

You may also be interested in...

Comments and Discussions

 
Praisevery nice Pin
BillW3314-Feb-19 3:15
professionalBillW3314-Feb-19 3:15 
QuestionYes Pin
Sacha Barber13-Feb-19 12:00
mvaSacha Barber13-Feb-19 12:00 
PraiseExcellent Article - my 5! Pin
Igor Ladnik21-Jan-19 9:16
mvaIgor Ladnik21-Jan-19 9:16 
PraiseExcelent! 5 stars!! Pin
Marco_BR15-Jan-19 13:42
memberMarco_BR15-Jan-19 13:42 
QuestionNice piece of work - thanks! Pin
Gerry19838-Jan-19 21:32
memberGerry19838-Jan-19 21:32 
Question8 second delay Pin
pkfox6-Jan-19 20:25
professionalpkfox6-Jan-19 20:25 
AnswerRe: 8 second delay Pin
Marc Clifton7-Jan-19 2:00
protectorMarc Clifton7-Jan-19 2:00 
GeneralRe: 8 second delay Pin
pkfox7-Jan-19 8:32
professionalpkfox7-Jan-19 8:32 
GeneralRe: 8 second delay Pin
Marc Clifton9-Jan-19 3:16
protectorMarc Clifton9-Jan-19 3:16 
GeneralRe: 8 second delay Pin
pkfox9-Jan-19 23:58
professionalpkfox9-Jan-19 23:58 
QuestionExcellent work! Pin
Mike Hankey6-Jan-19 12:39
professionalMike Hankey6-Jan-19 12:39 
AnswerRe: Excellent work! Pin
Marc Clifton6-Jan-19 14:32
protectorMarc Clifton6-Jan-19 14:32 
AnswerRe: Excellent work! Pin
Marc Clifton12-Jan-19 10:18
protectorMarc Clifton12-Jan-19 10:18 
QuestionGive the poor servers some time to catch up to all the images Pin
Marc Clifton6-Jan-19 12:00
protectorMarc Clifton6-Jan-19 12:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01 | 2.8.190306.1 | Last Updated 11 Jan 2019
Article Copyright 2019 by Marc Clifton
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid