5/5 - (2 votes)

A long time ago, when Linux was still at kernel 2.6 and PHP5 was a breath of fresh air, I first became interested in the world of web technologies. I read textbooks, articles, hung out on forums, but still could not understand much how the code that I see on the screen turns into magical sites with buttons, forms and animations. I learned about LAMP and its analogues for Windows, and found out that, it turns out, there are hosting sites where such sites are hosted. As soon as the external Internet appeared without traffic, I hastened to transfer my local projects to the outside world, simultaneously learning about the wonderful FTP protocol. It was just a world of magical discoveries for me, especially when I learned that you don’t need to write your forum from scratch, but you can use something from phpBB, vBulletin and other ready-made engines.

The main thing from all this is that over time I understood in general terms how hosting works, that there is a treasured public_html folder where you need to transfer all the files, create a table through PhpMyAdmin and that’s it – here you have a working website for further use. Many years later, when I plunged into the “fascinating” world of finance, business and procurement, I realized that I still wanted to develop software products, and not compile reports that needed to be passed on to the top.

And when I switched to the world of .NET and began studying it, the skills listed earlier played a cruel joke on me – for a long time I could not understand how I could find hosting for .NET applications. Why is it that all the hosting sites I know easily provide the opportunity to deploy PHP applications, even offering some pre-installed versions of the CMS, but in the daytime you can’t find hosting for .NET. My misunderstanding of the principle of deploying applications was aggravated by articles that suggested placing them in suitable services like Heroku, Digital Ocean or Azure – it’s so simple and cheap… Then, of course, I realized that such a folder does not exist, and applications are not “hosted” at all (on then they are applications), but let’s be honest – seniors are not born with knowledge and understanding of such things, understanding of obvious things does not immediately come to everyone.

Therefore, as a logical continuation of the previous article , where we launched our applications locally, we will transfer them to an external server and deploy a database using ssh and git, consider specific examples via Github, and at the same time, in practice, we will find answers to the following questions, which are most likely , are puzzling to newbies learning .NET programming. The series of articles is intended to address the following questions:

  • How to transfer the code to an external server and run it?
  • How to connect an SSL certificate?
  • How to deploy your database?
  • How to safely store sensitive data and use it on the server?
  • How to automate application deployment?

Specifically, in this article we will look at the basic principles of publishing without unnecessary problems. In the following articles we will analyze various tools that allow you to run several applications simultaneously in containers, deploy a database, connect a certificate and set up CD process automation. In general, we will complicate the solution as the series of articles progresses.

So, let’s start from the very beginning and the simplest thing – create an empty application using the current .NET 8.0.

dotnet new razor -n SimpleApp cd ./SimpleApp dotnet run

After this, the browser will launch along the path specified in ./Properties/launchSettings.json. In my case it is http://localhost:5144 . Upon transition, we will see a standard greeting.

Let’s say we have a domain name and we now want to make the application available at our address and available 24/7, how can we do this? First you need to rent a VPS; absolutely any will do for our simple and empty application. As the operating system to install, select something from the Debian family, for example Ubuntu 22.04. After paying for the tariff, you will be given access to the control panel, where the IP address will be indicated where you can access the dedicated machine, and also link your domain to this address.

I’ll point out the obvious here, but believe me, for beginners it’s not always obvious and understandable. Essentially, deploying .NET applications in some external environment, be it a cloud or a VPS, is no different from simply running an application on your machine. How you run an application via dotnet run locally (or through an IDE, or via docker) – the same must be repeated, but on a remote machine provided by the provider. You don’t need any special environment, you just need a computer with a pre-installed operating system where you can build and run the application.
And all deployment of .NET applications and deployment comes down to simply transferring code from your machine (or from a version control system) to some external virtual machine. It’s simple.

Then perhaps you have a logical question: “Why then the complexity with Heroku, Azure and other specialized services for hosting .NET applications? Why use them then, if you can simply “copy” the code to the VPS and run it?” And, to answer briefly, in our case such services are not needed, just as they are not needed for a large number of production solutions. In addition to the fact that they are more expensive than regular VPS rental (if we are talking about long-term hosting), their main advantage is the automation of the deployment of your applications after changing the code, which can be configured on a regular VPS by using various tools, such as Jenkins for more complex tasks, or using Github / Gitlab in simpler cases. Again, if you are interested in more details, google CI/CD and everything related to it. This is a separate extensive topic and we will not consider it now.

Copy using SSH

So, returning to our main question, how to copy the code to a rented VPS? There are two ways, as usual, one is simple, the other is correct. Let’s start with a simple one, since for some simple one-time launches it may also be sufficient. This is a normal copying of your project via ssh to a rented VPS.

scp -r ~/SimpleApp [email protected]:/root/

where SimpleApp is the folder with your project, root is the account name for access via ssh to the VPS, is the VPS address, :/root/ is the folder on the VPS where you should copy the code (specify the one that is convenient for you).

It’s worth clarifying here that using root is a bad practice, but by default it is the only entry that exists in the system. After creating the VPS, you should create another account, allow it to access sudo, disable the ability to connect remotely via root, and also change the port from 22 to something different. This is not difficult to do, you can find out exactly how in Google or GPT. If suddenly something goes wrong, then you always have the opportunity to connect via VNC through the virtualization control panel.

After entering the root password, the project folder will be copied and available at /root/SimpleApp. Connect to the VPS via ssh and check.

ssh [email protected]
# ls /root/ | grep 'SimpleApp'

There is absolutely nothing left to do – go to the folder and launch the application. To do this, just install the necessary launch environment for .NET applications.

sudo apt-get update && \
  sudo apt-get install -y dotnet-sdk-8.0

The environment is installed, we can go to the folder and launch the application.

cd SimpleApp && dotnet run
warn: Mi-crosoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {bc671e4e-e40d-4f31-a79e-4badce1e734f} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5144
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /root/SimpleApp


The launch was successful, which means our application is now available locally at the address, we check it through lynx.


lynx localhost:5144

Unfortunately, by default our application will not be accessible from the outside.

netstat -tuln
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0*               LISTEN     
tcp        0      0    *               LISTEN     
tcp        0      0  *               LISTEN     
tcp        0      0 *               LISTEN     
tcp6       0      0 ::1:5144                :::*                    LISTEN     
tcp6       0      0 :::22                   :::*                    LISTEN     
tcp6       0      0 :::5555                 :::*                    LISTEN     
udp        0      0 *                          

In order for our application to be accessible from the outside via a domain name or IP address, we need to use a reverse proxy. Nginx is perfect for this. Setting it up is not difficult, and if you use ChatGPT, it’s generally simple. It needs to be installed and configured to your liking (in the example, redirecting incoming external requests on port 80 to our local 5144).

Installing and configuring Nginx

If NGINX is not already installed on your server, you can install it using the package manager  apt on Ubuntu:

apt update
apt install nginx

Create a new configuration file for your web application in the directory  /etc/nginx/sites-available/ and create a symbolic link to it in  /etc/nginx/sites-enabled/to enable the configuration. In our specific example, this is a web application running on port 5144:

nano /etc/nginx/sites-available/mydotnetapp

Add the following configuration to it

server {
    listen 80;
    server_name asyncnoway.ru; # Замените на ваш домен или IP-адрес
location / {
    proxy_pass http://localhost:5144; # Перенаправление на ваше .NET приложение
    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;


Adding a link

ln -s /etc/nginx/sites-available/mydotnetapp /etc/nginx/sites-enabled/

Checking for errors

nginx -t

And if everything is ok, then restart the service

systemctl restart nginx

Now NGINX will accept incoming connections on port 80 and redirect them to your .NET web application running on port 5144. We can connect by domain name or IP (depending on what you added to/etc/nginx/sites-available/mydotnetapp)

Best the enemy of the good.
Of course, this method can hardly be called good, and I in absentia agree with everyone who is outraged by this approach. However, if you need to quickly deploy and check something, and you do not use a version control system, then this is quite an effective method. Ideally, it is better, of course, not to use dotnet run, since the application is launched in the Development environment, but still make a release and run it in the Production environment. Whether you need this specifically is up to you to decide; you can easily find it on the Internet.

Copying using GIT

However, it is worth understanding that the above method has several significant disadvantages. Firstly, the speed of copying via ssh is quite slow, which means that as the project grows, copying will take more and more time. Secondly, if the project is live and you constantly change something in it, then with each change you will have to copy all the files. Or copy only the changed files, but you need to somehow record their changes. In fact, this is exactly what git is for. When using a version control system on a VPS, only changed files will be updated, pulling updates via git pull from the code storage service. Using Github or Gitlab is purely a matter of taste, and in the context of our task there is only one significant difference between them: when trying to clone or update a private repository for Gitlab, it is enough to enter the login password for your account, but for GitHub this option is not available due to security reasons, so you will have to worry about creating keys and adding them. If you are using a public repository, then there is no difference. Therefore, in the future I will use a private Github repository, but all actions are completely similar for Gitlab, with the exception of adding an authentication key. Let’s get started.

The easiest way is to submit the project using any IDE – VSCode, Rider or Visual Studio. In such systems, you can save the account token once and avoid access problems when working with Github, and all interaction will be built through pressing buttons, but we will analyze all this through the console without being tied to any development environment.
We need to create a private repository in Github with the name of our application – for simplicity, just on the Github website. Then we will create a local repository, make the first commit and push the application to the remote repository.

dotnet new gitignore
The template "gitignore dotnet file" has been successfully created.

git init
Initialized empty Git repository in /Users/vasjen/SimpleApp/.git/
git add . && git commit -m "Init"
git remote add origin https://github.com/vasjen/SimpleApp.git
git push -u origin master
Enumerating objects: 98, done.
Counting objects: 100% (98/98), done.
Delta compression using up to 4 threads
Compressing objects: 100% (94/94), done.
Writing objects: 100% (98/98), 914.56 KiB | 2.34 MiB/s, done.
Total 98 (delta 33), reused 0 (delta 0)
remote: Resolving deltas: 100% (33/33), done.
To https://github.com/vasjen/SimpleApp.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

The upload was successful, and our project with our commit appeared in our remote repository. Now, in order to transfer the application, you just need to clone the repository via git clone, and after changing the code, update them via git pull. But, as I said earlier, since our repository is private, it is accessible only to you by default, which means you must explicitly specify your account for authentication on the VPS. Switch to the VPS console

git clone https://github.com/vasjen/SimpleApp
Cloning into 'SimpleApp'...
Username for 'https://github.com': vasjen
Password for 'https://[email protected]': 
remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/vasjen/SimpleApp/'

As we can see from the logs, this cannot be done simply by entering a login-password combination, unlike Gitlab. Therefore, you need to create an SSH key and point it to Github

ssh-keygen -t rsa -b 4096 -C "[email protected]" #replace with your account name
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_rsa
cat ~/.ssh/id_rsa.pub

We completely copy and paste the output key into your Github profile, in the keys section . We indicate a convenient name, mark the key as authorization. Let’s try to clone again via ssh

git clone [email protected]:vasjen/SimpleApp.git
Cloning into 'SimpleApp'...
The authenticity of host 'github.com (' can't be es-tablished.
ED25519 key fingerprint is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'github.com' (ED25519) to the list of known hosts.
remote: Enumerating objects: 98, done.
remote: Counting objects: 100% (98/98), done.
remote: Compressing objects: 100% (61/61), done.
remote: Total 98 (delta 33), reused 98 (delta 33), pack-reused 0
Receiving objects: 100% (98/98), 914.56 KiB | 1.92 MiB/s, done.
Resolving deltas: 100% (33/33), done.

Now we can work with our private repository. In case we add something to the project and push it into the repository, we can easily pull up the changes without having to copy the entire project from our computer.

git pull
Already up to date. # the project is currently up to date

Next, we repeat the steps from the beginning – launch the application in a way convenient for you, be it a simple launch through dotnet runor release of the application.

Application Containerization

As long as we are inside the VPS, everything works as it should. But as soon as we break the SSH session, our application will stop working, which is something we would hardly want. I suggest sticking to application launch standards and creating a dockerfile. At the same time, we’ll test pulling project changes through Git. Let’s create a simple dockerfile for our application and place it next to the rest of the project files.

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build

COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:8.0
COPY --from=build /app ./


ENTRYPOINT ["dotnet", "SimpleApp.dll"]

Now let's update our repository, commit the changes and push them to our Github

git commit -m "added dockerfile"
git push
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 457 bytes | 45.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/vasjen/SimpleApp.git
   e7ae45d..66da0a3  master -> master

We connect to the VPS, go to the folder with the project and pull up the repository updates.

ssh [email protected]
cd SimpleApp
git pull
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), 437 bytes | 437.00 KiB/s, done.
From github.com:vasjen/SimpleApp
   e7ae45d..66da0a3  master     -> origin/master
Updating e7ae45d..66da0a3
 dockerfile | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)
 create mode 100644 dockerfile

We collect the application image and launch the container with it, hang it on the same port 5144

docker build -t simpleapp .
docker run -p 5144:8080 -d simpleapp

Now our application is running in a container, locally available on the same port 5144, and will be available after exiting ssh.