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:
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
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;
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
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.
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
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.
Next we can create an A record within the DNS provider to point our chosen subdomain to our public IP address.
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!