Skip to content

Deployment — Server Requirements

What a production server must provide. Every requirement below traces to something in the code or the publish profiles; where the source cannot tell us — hardware sizing, for instance — this page says so rather than guessing.

Platform

Requirement Why
Windows Server System.Drawing.Common 6.0 is Windows-only from .NET 6 onward and is used for images and signatures; PDF export shells out to wkhtmltopdf.exe; and the code reads and writes hard-coded C:\upload\… paths.
IIS with the ASP.NET Core 6.0 Hosting Bundle The application is published framework-dependent (SelfContained = false) for net6.0, so the server supplies the runtime. The Hosting Bundle installs the runtime and the ASP.NET Core Module that IIS uses to host the app in-process (the .NET 6 default; the project does not override AspNetCoreHostingModel).
A site of its own, at the root of its host name The views contain about 130 root-absolute URLs (href="/…", action="/…", url: "/…") across 89 files, and the SignalR client connects to withUrl("/hub"). Under an IIS sub-application such as https://host/erp/ those all point outside the app.
Web Deploy (remote agent service) Both publish profiles use MSDeploy with MSDeployPublishMethod = RemoteAgent. See Build & Publish.
SQL Server Three databases — see Installation — Database Setup.
wkhtmltopdf in wwwroot\Rotativa\ Every PDF export. Not in the repository; must be placed by hand (R5-3) — and protected from deletion on publish (R4-3).

Views/web.config is not the site's web.config

The repository contains NKO.Main/NKO.Main/Views/web.config. It is ASP.NET MVC 5 configuration (System.Web.Mvc 5.2.3, System.Web.WebPages.Razor) left over from an earlier version of the application, and ASP.NET Core ignores it. The real web.config at the site root is generated by dotnet publish.

.NET 6 is out of support

The runtime this application needs stopped receiving security patches in November 2024 (R1-7). Installing the 6.0 Hosting Bundle on a new server is installing an unpatched runtime.

The application pool

The three background schedulers — Quartz (UpdateNextUserJob at 03:00), Hangfire, and BirthdayGreetingsJob — all run inside the web worker process. When IIS stops that process, they stop with it, and none of them catches up afterwards. The pool must therefore be kept alive.

Setting Value Why
.NET CLR version No Managed Code ASP.NET Core does not use the CLR integrated pipeline
Start mode AlwaysRunning Start the process without waiting for the first request
Idle time-out 0 (disabled) The default 20 minutes would stop the nightly job and the birthday mail on a quiet server
Site: Preload enabled True Runs Startup — and so the schedulers — immediately after a recycle
Maximum worker processes 1 See One instance only
Load User Profile True Lets ASP.NET Core persist its Data Protection keys — see below

These are standard IIS settings recommended for ASP.NET Core apps that run background work; the code-level reason they matter here is that no scheduler in this application has durable state or catch-up (R1-3).

Data Protection keys

ASP.NET Core encrypts the sign-in cookie and every anti-forgery token with its Data Protection keys. The application configures none — there is no AddDataProtection() call — so where the keys live depends on the host. Under IIS they are persisted only when the app pool loads its user profile (or the pool's registry hive has been provisioned); otherwise they are kept in memory and a new set is generated on every recycle. The visible symptom is that every recycle signs every user out and any form that was open fails its anti-forgery check. Setting Load User Profile avoids this. Tracked as part of R4-5.

Permissions for the pool identity

Path Access Why
The site folder Read Normal hosting
wwwroot\Rotativa\ Read and execute Runs wkhtmltopdf.exe
C:\upload\ (whole tree) Read and write Profile images, exports and templates — see below

The C:\upload folder tree

The code reads and writes these hard-coded paths. Nothing in the application creates them — there is no Directory.CreateDirectory call anywhere — so each must exist before the feature that uses it runs, or that feature throws.

Path Used by Direction
C:\upload\nko\files\userImages\ Employee profile images — AccountController (upload), BaseController.GetImage, WareBaseController.GetImage, GeneralController read / write
C:\upload\nko\templates\e-sign.pdf FilesController.DownloadTemplate — the e-signature request form read — file must be placed
C:\upload\nko\templates\contract.xlsx HumanResourceController — employee contract export read — file must be placed
C:\upload\ataa\ware\asset\ Asset and stock exports — AssetController, StockController, ExportHelper write
C:\upload\ware\assetDes\ Destruction export — DestructionController write
C:\upload\ware\asset\ Destroyed-assets report — ExportHelper write

nko comes from SiteSettings.LowerSiteName; the rest are literal strings.

Three different roots, one of them another product's name — R4-2

The same kind of file lands under C:\upload\nko\…, C:\upload\ware\… or C:\upload\ataa\ware\…. "Ataa" is not this product — it also appears in .gitignore alongside Ataa.Main and Ataa.Warehouse, which suggests this code was derived from a sibling project and those paths were never renamed. They still work, as long as the folders exist.

Network

Direction Endpoint Port Used for
Inbound the site 443 (and 80, redirected) Browsers. UseHttpsRedirection and — outside Development — UseHsts are on, so a TLS certificate is required.
Outbound SQL Server 1433 All three databases
Outbound AWS S3 (the region in StorageConfigs) 443 Every document upload and download
Outbound smtp.office365.com 587 All notification e-mail. The host is hard-coded in EmailManager.
Loopback the site's own public hostname 443 PDF headers and footers — see below

No outbound access to data.fixer.io is needed: the exchange-rate client is in the code but is never called (R7-17).

The server must be able to reach itself

PDF exports in JobsController (four of them) and AdvanceController pass wkhtmltopdf switches like:

"--header-html \"" + Url.Action("ExportationRotativeHeader", "Document", null, "https") + "\""

Url.Action(..., "https") builds an absolute URL from the host name of the current request. wkhtmltopdf then runs as a separate process on the server and downloads that URL. So:

  • the server must resolve its own public host name and be allowed to connect to it (some firewalls and load balancers block this "hairpin" connection);
  • the certificate must be one wkhtmltopdf's own TLS stack accepts;
  • the site must be served over HTTPS — the scheme is hard-coded.

If any of these fail, those PDFs come out without their header and footer, or fail outright. These two endpoints are [AllowAnonymous] precisely because wkhtmltopdf has no user session.

WebSockets

Real-time notifications use SignalR (/hub). Enable the IIS WebSocket Protocol feature; without it SignalR still works but falls back to long polling, which holds a request open per connected user.

One instance only

The application keeps state inside its process, so it must run as exactly one process on one server:

State held in-process What breaks with two processes
Quartz uses in-memory storage UpdateNextUserJob fires at 03:00 in every process
BirthdayGreetingsJob timer Every employee receives the greeting once per process
SignalR has no backplane A notification reaches only users connected to the same process
Data Protection keys not shared A cookie issued by one process is rejected by another — random sign-outs

That rules out an IIS web garden (Maximum worker processes > 1) as well as a server farm. Tracked as R4-5.

Sizing

The source gives no basis for CPU, memory or disk sizing, and there are no load tests. Two facts are relevant when choosing: documents live in S3, not on the server, so local disk holds only the C:\upload tree and logs; and the top bar runs several unindexed NextUserId counts on every page render (R2-2), so database capacity matters more than web-server capacity.