.Net Application runs as service, but not recognised by Nginx with 404

Hi everyone, I am facing a problem where my existing setup returns 404 error when accessing the webpage:

I am trying to setup working .NET application as a service to be accessed from one of the domains created with HestiaCP.

HestiaCP setup

  • Created User: auris
    [ Role: user ][ Package: default ]

  • Created Domain for this user xxx.co.uk
    [ Redirection: enabled ] [ SSL: enabled/verified ]
    [ Nginx: no-php ] [ PHP-FPM: no-php ]
    [ Fast CGI: disabled ]

Debian 11 setup

  • Installed .Net runtimes and SDKs for .Net 5
  • Created a service: kestrel-DigitalNotebook.service
  • Added user auris to www-data group
[Unit]
Description = DigitalNotebook framework

[Service]
WorkingDirectory=/home/auris/web/xxx.co.uk/public_html
ExecStart=/bin/dotnet /home/auris/web/xxx.co.uk/public_html/DigitalNotebook.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-5-digital_notebook
User=auris
Group=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

[Install]
WantedBy=multi-user.target

.NET 5 setup

  • My Startup File below:
using DigitalNotebookData;
using NotebookServices;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using DigitalNotebookData.Models;
using Microsoft.AspNetCore.HttpOverrides;

namespace DigitalNotebook
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<ForwardedHeadersOptions>(options =>
            {
                options.ForwardedHeaders =
                    ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
            });

            services.AddControllersWithViews();
            services.AddSingleton(Configuration);
            services.AddScoped<INotebookEntry, EntryContentService>();
            services.AddScoped<INotebooks, NotebookContentService>();
           

            services.AddDbContextPool<NotebookContext>(options => options.UseMySql(Configuration["ConnectionStrings:NotebookDataConnectionMysql"], ServerVersion.AutoDetect(Configuration["ConnectionStrings:NotebookDataConnectionMysql"])));

            services.AddIdentity<ApplicationUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
               .AddDefaultUI()
               .AddDefaultTokenProviders()
               .AddEntityFrameworkStores<NotebookContext>();



            services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            })
            .AddCookie(options =>
            {
                options.LoginPath = "/Identity/Account/Login";
                options.AccessDeniedPath = "/denied";
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
            });

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
                endpoints.MapRazorPages();
            });
        }
    }
}

Service display [ Be aware: xxx and xxx2 are different domain names ]

kestrel-DigitalNotebook.service - DigitalNotebook framework
     Loaded: loaded (/etc/systemd/system/kestrel-DigitalNotebook.service; enabled; vendor preset: enabled)
     Active: active (running) since Sat 2022-02-05 18:45:16 -09; 10h ago
   Main PID: 394696 (dotnet)
      Tasks: 15 (limit: 9371)
     Memory: 55.9M
        CPU: 3.285s
     CGroup: /system.slice/kestrel-DigitalNotebook.service
             └─394696 /bin/dotnet /home/auris/web/xxx.co.uk/public_html/DigitalNotebook.dll

Feb 05 18:45:16 xxx2.co.uk dotnet-5-digital_notebook[394696]: warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
Feb 05 18:45:16 xxx2.co.uk dotnet-5-digital_notebook[394696]:       No XML encryptor configured. Key {234200ba-8e94-4c14-a300-e038190d3b7f} may be persisted to storage in unencrypted form.
Feb 05 18:45:16 xxx2.co.uk dotnet-5-digital_notebook[394696]: info: Microsoft.Hosting.Lifetime[0]
Feb 05 18:45:16 xxx2.co.uk dotnet-5-digital_notebook[394696]:       Now listening on: http://localhost:5000
Feb 05 18:45:16 xxx2.co.uk dotnet-5-digital_notebook[394696]: info: Microsoft.Hosting.Lifetime[0]
Feb 05 18:45:16 xxx2.co.uk dotnet-5-digital_notebook[394696]:       Application started. Press Ctrl+C to shut down.
Feb 05 18:45:16 xxx2.co.uk dotnet-5-digital_notebook[394696]: info: Microsoft.Hosting.Lifetime[0]
Feb 05 18:45:16 xxx2.co.uk dotnet-5-digital_notebook[394696]:       Hosting environment: Production
Feb 05 18:45:16 xxx2.co.uk dotnet-5-digital_notebook[394696]: info: Microsoft.Hosting.Lifetime[0]
Feb 05 18:45:16 xxx2.co.uk dotnet-5-digital_notebook[394696]:       Content root path: /home/auris/web/xxx.co.uk/public_html

So, as a result of all of that, I get 404 when I go to xxx.co.uk. Service is running. I researched and found some topics via google on errors dispalyed @ service display, but that should not be an issue as this setup normally works in Ubuntu with Nginx entry as bellow:

server {
	server_name xxx.co.uk www.xxx.co.uk;
	root /var/www/auris;

	location / {
        proxy_pass         http://localhost:5000;
        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;
    }

    listen 80; # managed by Certbot

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/xxx.co.uk/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/xxx.co.uk/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

And Debian should be on the same strain of Linux as Ubuntu, so this may mean that Nginx setup is incorrect as it is automagically generated every time, even if I decide to change it manually. So, any suggestions are appreciated and let me know if you need more info.

hey, thanks for the information…

did you check/are you sure, that your app is running on port 5000?
what does the nginx logfile say?
have you tried allowing your port 5000 for 127.0.0.1 in iptables/firewall?

What is the content of no-php template in Nginx?

Should be a good starting point…

Only need to change:
proxy_pass https://%ip%:8083;

to proxy_pass https://%ip%:5000;

did you check/are you sure, that your app is running on port 5000?

How should I check that? I tried: xxx.co.uk:5000 and xxx.co.uk:5001 [ssl]. Does not work.

what does the nginx logfile say?

[ /var/log/nginx/error.log.1 ] It has no related errors for today or for this domain, otherwise its errors for SSL handshakes from previous SSL activations that failed.

have you tried allowing your port 5000 for 127.0.0.1 in iptables/firewall?

Ok just added one:
tempsnip

Does not work still.

Edit: Just changed Server IP to 127.0.0.1 → no success

It says that changes will be lost anyways, should I still change this?

netstat -tulpn should list the ports that are listening and a service name, so you can identify…

if your app is actually listening on two different ports and 5001 is the ssl one, then maybe you either want two different vhosts and proxy rules aka port 80 → port 5000 and port 443 → 5001 or you need to check, which one of both your app really wants to run on and server it’s content…

maybe on port 5000 it’s just listening and redirecting? then your main focus and proxy_pass should be towards https://localhost:5001.

for the firewall rule, you probably want 127.0.0.1 instead of the server IP, as nginx is coming from localhost when connecting to your app. if there are more ports than just 5000 you want to add these too (can be done within one rule, comma separated)

also for testing purposes, you can open up the firewall for these ports incoming from 0.0.0.0/0 and then indeed try calling the url:port directly from your browser…

Just wanted to add some information that may help:

  • I am running this server myself from home and I have ports all ports open on router including 5000, 5001, 443, 80

  • I am not using Apache at all, just Nginx.

Now some tests:

netstat -tulpn

Returns

tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      -

I did open 5001 as well, but its not showing.

I am also quite confused why my app wants port 5000, because I removed it from my appsettings.json file in .net project. Maybe its default → will investigate further.

Does Nginx has vhosts settings? [ obviously I did not tell im not using apache ]

Done that [ added both 5000,5001 ], with 127.0.0.1 as IP → no luck

Tried 0.0.0.0/0 and went for IP:PORT also XXX:PORT and XXX2:PORT with no success → Connection Refused. Checked for ports: 5000 and 5001 even if 5001 does not apear in netstat.

After all your suggestions, I think I should go to my APP and define ports, so I would definatelly know what it wants to operate. Then maybe do vhosts as you suggested or make a ?template? as @eris Said?

Maybe there is some other log file that would clarify things apart from Nginx?

Edit: This setup is for one of my sites only, the rest will be running normal PHP like Laravel, etc… Meaning, that I do not want to make changes globally, not to screw up other things :smiley: I wish I could just add Nginx block as I did in Ubuntu and that would work :smiley:

yeah that helps quite a bit.

so far your app is only listening on port 5000 on localhost as you correctly identified already with netstat.

with that it does make sense that even when calling your url:5000 from home via browser nothing happens. the app is simply not listening on the public address - and that is correct, as you most likely do not want your app listen directly.

so here is what I would do:

  • first remove the public opening from firewall again and only leave a rule for allowing port 5000 and 5001 from localhost (though it does not help yet)
  • from the server cli run a curl http://localhost:5000 and curl http://localhost:5000 - this should obviously give you something back, directly from the app, without nginx in between. probably closest you can get to see, if the app is responsive at all.
  • depending on the outcome try to change ports/settings within the app (as you also already are thinking about)

only after getting something that makes sense with the curl I’d move on to setting up nginx properly…

about that: ‘vhosts’ might be the wrong term, sorry. it’s rather the server blocks with different listen directives. what you posted above is a combined one to handle 80 and 443 in one go. I would not do that. instead nginx in hestia normally uses two config files, one for port 80 aka non-ssl and one for port 443.
I would use that to create a template - copy a default one to a different name and select that later from the domain setting. but keep in mind that it’s gonna be two files. .tpl .stpl
with that you can proxy port 80 to port 5000 and 443 to 5001 - if that is, how it’s supposed to be for your app and you got that configured on the app level correctly.

of course, there are different ways to handle it, so this does not mean my approach would be the only correct or working one. if you have this running at home, maybe compare the configs/setup. also check how the proxy config is done in apache, if you are using that - it might read differently, but probably is not that different at all :wink:

So I have combined @falzo and @eris suggestions and here is where I am at:

from the server cli run a curl http://localhost:5000

This works and I got a RAW representation of my app landing page, so I think thats good news already.

I would use that to create a template - copy a default one to a different name and select that later from the domain setting. but keep in mind that it’s gonna be two files. .tpl .stpl

Did this, but instead copying existing preset for .tpl and .stpl files I used @eris github repo, downloaded to templates, renamed and edited to look like this:

dotnet.tpl

#=======================================================================#
# Default Web Domain Template                                           #
# DO NOT MODIFY THIS FILE! CHANGES WILL BE LOST WHEN REBUILDING DOMAINS #
#=======================================================================#

server {
    listen      %ip%:%proxy_port%
    server_name %domain_idn% %alias_idn%;
    index       index.php index.html index.htm;

    include %home%/%user%/conf/web/%domain%/nginx.forcessl.conf*;
    location / {
    return 301 https://%domain_idn%/;
    }

    location /error/ {
        alias   %home%/%user%/web/%domain%/document_errors/;
    }

    location @fallback {
        proxy_pass      http://%ip%:5000;
        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;
    }

    location ~ /\.(?!well-known\/|file) {
       deny all;
       return 404;
    }

    include     /etc/nginx/conf.d/phpmyadmin.inc*;
    include     /etc/nginx/conf.d/phppgadmin.inc*;

    include %home%/%user%/conf/web/%domain%/nginx.conf_*;
}

dotnet.stpl

#=======================================================================#
# Default Web Domain Template                                           #
# DO NOT MODIFY THIS FILE! CHANGES WILL BE LOST WHEN REBUILDING DOMAINS #
#=======================================================================#

server {
    listen      %ip%:%web_ssl_port% ssl http2;
    server_name %domain_idn% %alias_idn%;
    index       index.php index.html index.htm;
    ssl_certificate      %ssl_pem%;
    ssl_certificate_key  %ssl_key%;
    ssl_stapling on;
    ssl_stapling_verify on;
    error_log  /var/log/%web_system%/domains/%domain%.error.log error;

    include %home%/%user%/conf/web/%domain%/nginx.hsts.conf*;

    location / {
        proxy_pass      https://%ip%:5001;
    }

    location /error/ {
        alias   %home%/%user%/web/%domain%/document_errors/;
    }

    location @fallback {
	proxy_pass      https://%ip%:%web_ssl_port%;
        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;
    }

    location ~ /\.(?!well-known\/|file) {
       deny all;
       return 404;
    }

    include     /etc/nginx/conf.d/phpmyadmin.inc*;
    include     /etc/nginx/conf.d/phppgadmin.inc*;

    include %home%/%user%/conf/web/%domain%/nginx.ssl.conf_*;
}

I assume that everything in %variable% are automatically fetched from some server config file. OR am I wrong?

Run Nginx test and got an error:

nginx: [emerg] invalid port in "192.168.x.xx:" of the "listen" directive in /etc/nginx/conf.d/domains/xxx.co.uk.conf:8

Same for the xxx.co.uk.ssl.conf and obviously I need to add ports. I already tested with added ports and they are rewritten by hestia to same exact problem.

Anything im missing here? Do we need to check logs?

1 Like

if you did not do that already, you can check the configs that are generated from your templates by looking at /home/user/conf/web/domain.co.uk/ (or using the links under /etc/nginx/conf.d/domains/ as stated in that error) and the according .conf / ssl.conf files there… as you correctly assumed the placeholders should be populated properly after selecting your template from the web-gui.

however, be aware, that there are different templates for installations that are nginx only vs those that have nginx as proxy+apache as backend. from the looks of it, the template you created is for the latter.
if you run nginx only, I think the variables for %proxy% won’t populate, as there is no proxy after all, but nginx is the main web/ssl service. I guess that is what’s causing the error here for you.

So now, when I read your Post for 3rd time, something clicked :smiley: I have wrong templates for what I have (Nginx without apache) and I need to elevate from a template that matches my configuration.

Some questions here:

  • Which existing template would you recommend to start editing from? [ default.tpl ? ]
  • Can I just use %web_port% instead of proxy_port?
  • How can I check what is actually behind those variables?
    [ Document that would show something like: %ip% = 82.111.25.158 ]
1 Like

to be fair I usually have nginx+apache2 and can’t remember having fiddled with the nginx only templates, but if you look at what is already there, it should give you a good idea about the available placeholders :wink:

just wanted to say, awesome to see your willingness in getting this done. and I think you are making good progress, keep on going!

for your questions. yes. default should be fine. get rid of most of the stuff in there and just put some proxy_pass directives into the main location block.
I am not sure about the placeholder, but should be visible in that correct default tpl anyway.
no idea if we have an actual documentation for that I am afraid… but obviously the templates are used to generate the final config files, so after applying it to your domain you can see the translated output in the config file per domain.

1 Like

Thanks to you as well man - where I would be without help.

Stepping from Ubuntu + Webmin to Debian + Hestia is a good challange in many ways :smiley:

I will play around with a copy of default preset and will see whats what. Btw, in your expertise, would you recommend me to add apache2 at this point and why?

I just do not think I really need it to host websites that are more likely static.

Use apache2 if you are going to host wordpresses with a lot of plugins or you can’t control what your clients will install.

Otherwise use nginx

The thing is that many wordpress plugins rely on .htaccess files and won’t work without it.

1 Like

Im going to post/edit this entry until someone reponds, due to the fact that im running into new issues and sometimes mitigate them before that friendly helping hand aproches :smiley:

Any clue why port 5001 does not show up using netstat -tulpn? This may become an issue as domain is already running SSL.

You don’t need 2 different ports to be active. Nginx can handle ssl as forward everything to http://ip.addr.xx.xx:5000

Thats exacly how I had it in Ubuntu → with one port 5000. That would mean I could delete .stpl file as redundant? Also, in my app I am using specific headers

services.Configure<ForwardedHeadersOptions>(options =>
            {
                options.ForwardedHeaders =
                    ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
            });

and

 app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
            });

Meaning that I still need this part of Nginx block:

        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;

In Hestia I believe my variables $ become %%?

Currently my block for .tpl looks like:

server {
    listen      %ip%:%web_port%;
    server_name %domain_idn% %alias_idn%;
    root        %docroot%;
    index       index.php index.html index.htm;
    access_log  /var/log/nginx/domains/%domain%.log combined;
    access_log  /var/log/nginx/domains/%domain%.bytes bytes;
    error_log   /var/log/nginx/domains/%domain%.error.log error;

    include %home%/%user%/conf/web/%domain%/nginx.forcessl.conf*;

    location / {
        proxy_pass      http://%ip%:5000;
    }

    location ~ [^/]\.php(/|$) {
        types { } default_type "text/html";
    }

    location /error/ {
 alias   %home%/%user%/web/%domain%/document_errors/;
    }

    location ~ /\.(?!well-known\/) {
       deny all;
       return 404;
    }

    location /vstats/ {
        alias   %home%/%user%/web/%domain%/stats/;
        include %home%/%user%/web/%domain%/stats/auth.conf*;
    }

    include     /etc/nginx/conf.d/phpmyadmin.inc*;
    include     /etc/nginx/conf.d/phppgadmin.inc*;
    include     %home%/%user%/conf/web/%domain%/nginx.conf_*;
}

Still stuck at 404 → its like my app is in Narnia XD

nginx handles the ssl termination for the client, so for sure you need the stpl aka ssl-conf.

think of it this way, the client requests https → ends up on port 443 of your server, where nginx is listening. and that part is configured via the according config. the regular tpl on the other hand might not need any proxy_pass at all but could be left to just redirect to https if someone accidentically runs your url without it.

internally you again have the choice where you want to proxy from, http://localhost:port or https://localhost:port - where port is just a number and if your app supports ssl internally or not depends on it handles certification and stuff then.

so, if your app is listening only on one port and expects to deal with ssl encryption, you should create a .tpl that listens on port 80 and merely handles a redirect to https://yourdomain
also an stpl that does the actual proxying to your app whether it be http:// or https://

sorry if the part with the two internal ports added to the confusion, it is just because you mentioned that earlier like this:

on another note I think I noticed another misunderstanding regarding iptables (firewall) and netstat. so netstat does not reflect your firewall changes. it just show services on your system that are listening on any ports. changing the firewall/iptables rules has no impact there, but only defines who or from where other accesses are allowed (aka nginx → your app)

splitting for better readability:

no, these variables ($host $http_upgrade) are nginx specific, Hestia does not touch that, just leave them as they are, even in the template, otherwise it will break.

1 Like

Trying the redirection method right now then :slight_smile: As I tried like 30 different configurations by now and it all ends up at 404

I am using:
return 301 https://%ip%$request_uri;