Linode offers a cheap Linux machine for as low as $5 USD. Blazor offers a lightweight binary messaging client-server framework for as low as 3 bytes per update. I am tired of Azure for simple things.


I used to pack and push a docker image to Heroku with Blazor Server and was very proud of myself. Earlier in 2022, Heroku shut down their free tier. In my opinion, good riddance to sad rubbish. They still charge to supply SSL certs on an application… screw them. Alternatively, many years ago I found out about Linode from one of Scott Hanselman’s blog posts. I love it. It is easy to setup and you get a wide range of distributions to choose from. Not to mention, from a traditional dotnet developer’s perspective, it opens you up to the rest of the world. That’s a good thing. I’m a “still too new to linux world that I probably will destroy the moon by accident” dotnet dev but I have a desire to deploy no-non-sense apps and have fun doing it.

Okay, get to it

This process assumes you got a Blazor Server application and have published a portable release. Should be in here someone if you’re unfamiliar: MS Docs - dotnet publish options

This also assumes you have linode setup with proper ssh access. Linode Getting Started

This is using the latest Ubuntu image on Linode at the time. 22.x

Setup a dotnet runtime

Well, if you want a portable dotnet app to run, you need the runtime. Let’s get that the command line way!

Log into your machine via ssh on whatever you like which is Terminal and nothing else. Yup yup.

Navigate to a folder you want to download temporary files into. Run the following to download the install script:

wget https://dot.net/v1/dotnet-install.sh

See more here: MS Docs - dotnet-install examples

This will either install the full sdk (which includes the runtime) or the runtime in our case. I’m going to install the .NET 7 runtime by specifying the short term release identifier STS:

./dotnet-install.sh --runtime dotnet --channel STS

This should place bits at $HOME/.dotnet based on their documentation. Follow up with the install by setting env vars if you like. In our case, it isn’t required.

See more here: MS Docs - dotnet-install Env vars

Setup Kestrel

You either need to specify the port that kestrel will operate on via the dotnet invoke using blah.blah.blah.dll --urls http://*:9000 (we can set this in the service file later on), at runtime in code or via appsettings.json (one supercedes the other so take note if you configure it twice). I specify mine in appsettings. In this case, since I am going to proxy via nginx, I can just set an appsettings.prod.json or something. For the sake of an example, I’m doing it in the standard appsettings. Add this kestrel config:

  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://*:8027"

The reverse proxy will handle https and communicate with kestrel via this port.

Publish and move code to your linode

This process is to serve something similar to publishing from VS (ie without a pipeline). You can use any of the many different ways to move files.

From here, just do your normal portable publish and copy the folder path.

You can push files and folder via SCP or something like that to a linux box. If you’re on Windows you can also use Filezilla which is a bit easier if you’re not a linux cli person and offers some commands to learn from. In this case, I’m going to use Filezilla.

In Filezilla, connect to your linode up top (where it says Host: Username: Password:) without using the port number. This will provide you with the folder structure (in the right panel where it shows Remote site:) you need to quickly navigate to a destination folder. Find a spot to deploy your application and take a note of the path. In the left panel, navigate to your publish folder (you might have copied it a minute ago). Publishing a fairly basic Blazor Server project as portable will have root files such dlls and a wwwroot folder. You can now select all of your files in the Local site: panel and r-click Upload. Filezilla will issue the necessary commands to move the files over.

Verify the runtime

Navigate to your app folder and run your app with the full dotnet path

/some/path/.dotnet/dotnet My.Blazor.App.dll

If you set env vars, then you don’t need to specify a path to dotnet.

Setup a Linux background service

I’m definitely not a linux expert. I know enough to worry myself that I’ve done something wrong. Thankfully, this part is fairly straight forward and a great start to learning a little bit more if you aren’t an expert. Most of what I know is thanks to Docker. Anyhow… let’s get at it..

Just like Windows, it’s a few easy configuration options to run a service. It begins with navigating to the systemd service directory:

cd /etc/systemd/system/

Here we are going to to create a new service file.

touch blazorapp.service

FYI, touch makes a file. You can ls here to see the other x.service files.

Now we’re going to edit this file the linux way!! Run the following:

nano blazorapp.service

Take a step back and look at that empty magic. Don’t worry its just a text file, nano is an editor, and we’re going to copy pasta some contents based on my git repo, blazorschool.com and this post.

In my repo, find the /deployment/files/app-web.service file and take a look. You can get the gist of it pretty easily. Follow the linuxhandbook.com link to find out more about the configurations available.

If you are awesome and have used Terminal to ssh and run nano, then you can copy pasta those contents into nano and press ctrl+s to write to the file.

Ubuntu nano allows ctrl+s to save and ctrl+x to exit. Not sure if this is standard but if you’re stuck in vim maybe call emergency services… or flip modes and :wq… idk.

Understanding the blazorapp.service file

I’ll give a quick run-down on the file I linked to create the service:

Description=app service description

Unit contains the description and some events that it may rely on. In this case, I defined the name and told it to wait until the network is available.

ExecStart=/path/to/dotnet Portable.Dotnet.dll

Service contains the bits that mostly configure it. The only one that isn’t self-explanatory is Type. In this case, it has something to do with being a service that emits notifications whereas a “simple” Type would not. Read more at the previously mentioned handbook website.


Install simply defines the services other system dependencies. In this case, default.target is “the system is ready for things to start running”. Again, network.target is simply “the system networking should be operational first”.

Some of this may not be required. Do your homework you linux guru!

Install and start up your service

Now that your service file exists in the expected folder, you can enable it by running:

systemctl enable blazorapp

blazorapp was the first part of the service file we created.. wasn’t it?

Now try to start it by running:

systemctl start blazorapp

You can check the logs by running journalctl -u blazorapp to see if it is running. You should see something like:

systemd[1]: Started blazorapp

Use the logs to see why we’re not running if you are getting exceptions. It’ll tell you why most likely.

Setup reverse proxy with nginx

You can use what you like. I will use nginx and show how to set up automated SSL.

Most of the setup I ninja’d from blazorschool.com down towards the bottom. This assumes you are running Ubuntu distro and does nginx fairly default-ly. The only caveat is this indicates sites-enabled is not the place to configure things but rather sites-available see stackoverflow. I left mine as-is per blazorschool’s instruction so far.

FYI That default file you deleted can be named the same. I assume it pulls all files here.

One differing point is that if you have www and root (e.g. blzr.dev and www.blzr.dev) pointing to your machine, then you need to ensure those are both specified in the server_name separated by a space or comma on 80 and 443.

Otherwise, ensure your kestrel port is correct. Remember, we’re going to proxy requests from 80 & 443 on https only into kestrel on http.

Remember to reload nginx. Ubuntu may complain that you changed files and it needs to restart the daemon (which it gives you the command to run).

Setup Let’s Encrypt OpenCert bot

This assumes you need a cert. You do you.

Stop nginx service:

systemctl stop nginx

Go here and follow directions until step 8: https://certbot.eff.org/instructions?ws=other&os=ubuntufocal

Instead of

certbot certonly --standalone

Run the nginx specific command:

certbot --nginx

Start nginx service:

systemctl start nginx

Follow the remaining instructions to ensure there is a task to auto-update your cert.

Try it out

Go to your domain.whatever and see it in action. If it didn’t work, it’s probably my fault. But you learned a lot and can fix it yourself ;)


Remember, this is a tiny, vanilla linux box with dotnet and Blazor. The ultimate dotnet dev stack for productivity. You could have done this on docker or any other 500 ways.

If you found this helpful, let me know @bitobrian on musk’s bird app. I have Mastadon but I’m too stupid to use it without feeling FOMO. It’s @bitobrian@twit.social if that is how it works.

Check out blzr.dev in the future too. I’m working on an all-things blazor site. It’s running on a $5 linode as described in this post.