Hosting a .Net API on a Raspberry Pi
Photo by Jainath Ponnala on Unsplash

Hosting a .Net API on a Raspberry Pi

Table of contents

Introduction

Cloud hosting has become fairly standard for deploying a variety of software over the past few years, and there are a number of options for hosting small personal projects.

The convenience and simplicity is great, but once you start getting out of the free tiers, prices add up fairly quickly with VMs, app services, and databases. Out of curiosity, I put a small comparison together for running an API with a Postgres database:

💡
If you don't mind the warmup time of serverless, and using MicrosoftSQL, you could probably get away with using Azure Functions and a serverless database for close to nothing. I wanted to have an "always-on" API with the ability to run scheduled tasks and avoid platform-specific code, though.

Considering costs for an entire year, self-hosting on a raspberry pi is going to be $70 cheaper than Digital Ocean and $380 cheaper than Azure, even with spending $165 on a raspberry pi kit that comes with an aluminum case, power cable, and 64g SD card.

Setup The Raspberry Pi

To get the API running on a Raspberry Pi, a few steps that need to be taken:

Build the Image

First, an image needs to be put on the pi. Most kits come with Raspbian already installed, but I wanted to use Ubuntu Server. Installation is pretty simple via the Raspberry Pi Imager.

Simply choose the device, OS, and storage device. After that, you can specify the wireless network, user password, and several other options.

At this point, you should be able to power up the pi and SSH into it

ssh <user>@<hostname / ip>

Next, we'll want to make sure that SSH is allowed through the firewall along with TCP on whatever port we're going to use for the API

ufw allow OpenSSH
ufw allow <api-port>/tcp
ufw enable

Install Postgres

Now we need to install postgres

sudo apt update
sudo apt install postgresql postgresql-contrib

Once postgres has installed, we can connect via

sudo -u postgres psql

Then we can create a new database and user for our API

CREATE DATABASE <database-name>;
CREATE USER <user-name> WITH PASSWORD '<password>';
ALTER USER <user-name> WITH SUPERUSER;
💡
Generally you'll only want to give the user access to the new database with the fewest permissions required, rather than making them a superuser. This was done for simplicity.

Install .Net

The last thing to install is the .Net runtime so the API actually runs.

sudo apt-get update & sudo apt-get install -y dotnet-runtime-6.0
💡
You can replace "6.0" with "7.0" or whatever other version, and can use dotnet-sdk instead of dotnet-runtime if you prefer to install the sdk

Build the API

Back on our dev machine, we need to publish our api and copy everything to the Raspberry pi.

Publish the API

First, we create a new framework-dependent publish profile targeting a folder for linux-arm64 from within visual studio.

Copy to the Raspberry Pi

With everything published, we can copy the output by using scp

scp -r C:\<publish-directory-path>\* <user>@<hostname / ip>:/home/<user>/<destination-directory>

Next, we need to ensure we're using the production configuration and can do so by setting the ASPNETCORE_ENVIRONMENT to Production.

sudo nano /etc/environment

# Add this to the end of the file
ASPNETCORE_ENVIRONMENT=Production

At this point, it's probably a good idea to test everything to make sure it's working. Navigate into the destination directory on the pi and run

dotnet <api.dll> --urls="http://0.0.0.0:<port>"

You should have a cursor waiting on the pi and be able to call one of the endpoints through a tool like postman. In my case I created a /version endpoint that at least lets me know the API is running by returning the api version.

Setup DNS

If you only want things running locally, this step isn't required. If you want to expose it to the internet however, we need a domain name and to point incoming requests to our public IP

DNS Host

There are a number of DNS providers from which you can purchase a domain name. I used to use Google Domains until they shut it down, but have since switched everything over to Cloudflare and love it.

First, find out your IP address by going to whatismyip.com or making a GET request to https://v4.ident.me

Next we can create an A record within the DNS provider to point our chosen subdomain to our public IP address.

Port Forwarding

Requests to the domain should now be going to our public IP, but at this point the router doesn't know what to do with it and nothing happens. To resolve this, we need to log into our router and forward requests on the appropriate port to our raspberry pi.

After making these changes, requests going to the specified domain and port should now make their way to our Raspberry Pi.

Create a Service

Earlier, we manually ran a dotnet command to get the API running. While this is ok initially, creating a service that starts the API every time the pi reboots would be much better.

To do so, we first make a script that runs the same command we typed earlier called startup.sh:

dotnet <api.dll> --urls="http://0.0.0.0:<port>"

Now, we can create a service that runs the script at startup

sudo nano /etc/systemd/system/<api>.service

# Contents should be similar to the following:
[Unit]
Description=<description>

[Install]
WantedBy=multi-user.target

[Service]
ExecStart=/bin/bash ./startup.sh
Type=simple
User=<user>
Group=<user>
WorkingDirectory=/home/<user>/<destination-directory>
Restart=on-failure

First, test that it works by starting and stopping the service with

systemctl start <api>.service
systemctl stop <api>.service

If, after starting the service, everything works correctly, we can then enable it on startup by running

systemctl enable <api>.service

And now we have an API running on our Raspberry Pi that will start automatically every time the machine restarts!

Kevin Williams

Springfield, Missouri
A full stack software engineer since 2018, specializing in Azure and .Net.