Deploying C# / .NET Apps (Kestrel + Nginx)
Deploying a C# / .NET app typically involves publishing the build artifacts with dotnet publish, running the app with Kestrel (the built-in .NET web server), and placing Nginx in front as a reverse proxy. Registering Kestrel as a systemd service enables automatic startup after a server reboot. This page uses a web app called kyo-dotnet — modeled after KOF fighter Kusanagi Kyo — as an example, and walks through the entire flow from publishing to service registration and Nginx configuration.
Syntax
# -----------------------------------------------
# Install the .NET SDK (Ubuntu / Debian)
# -----------------------------------------------
# Add the Microsoft package repository
wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
# Update the package list and install the .NET SDK
sudo apt update
sudo apt install dotnet-sdk-8.0
# Verify the installation
dotnet --version
# -----------------------------------------------
# dotnet publish — publish the app
# -----------------------------------------------
# dotnet publish [project path]
# → Builds the app for release and outputs artifacts to the publish directory
# Options:
# -c Release : Build in Release configuration (optimized)
# -r linux-x64 : Specify the target runtime
# --self-contained : Bundle the runtime (true: bundled / false: not bundled)
# -o {output dir} : Specify the output directory
#
# Note: Run this from a user directory that does not require sudo
# Example: dotnet publish ./KyoApp -c Release -r linux-x64 --self-contained false -o /var/www/kyo-dotnet
cd /home/kyo/KyoApp
dotnet publish -c Release -r linux-x64 --self-contained false -o /var/www/kyo-dotnet
# -----------------------------------------------
# Register Kestrel as a systemd service
# -----------------------------------------------
# Create /etc/systemd/system/{service name}.service and
# run systemctl enable to enable automatic startup
# Example: sudo systemctl enable kyo-dotnet.service
sudo nano /etc/systemd/system/kyo-dotnet.service
# -----------------------------------------------
# Enable and start the service
# -----------------------------------------------
# Notify systemd of the new or changed service file
sudo systemctl daemon-reload
# Register the service to start automatically on OS boot
sudo systemctl enable kyo-dotnet.service
# Start the service immediately
sudo systemctl start kyo-dotnet.service
# Check the running status
sudo systemctl status kyo-dotnet.service
# -----------------------------------------------
# Nginx reverse proxy configuration
# -----------------------------------------------
# Create /etc/nginx/sites-available/{config file name}
sudo nano /etc/nginx/sites-available/kyo-dotnet.conf
# Create a symbolic link in sites-enabled to activate the configuration
sudo ln -s /etc/nginx/sites-available/kyo-dotnet.conf /etc/nginx/sites-enabled/kyo-dotnet.conf
# Validate the Nginx configuration syntax
sudo nginx -t
# Apply the new configuration
sudo systemctl reload nginx
# -----------------------------------------------
# appsettings.Production.json — production settings
# -----------------------------------------------
# Automatically loaded when ASPNETCORE_ENVIRONMENT=Production
# Use this file to override database connection strings and log levels
# Both appsettings.json and appsettings.Production.json are merged,
# with the latter taking priority
Syntax Reference
| Step | Description |
|---|---|
dotnet publish -c Release -r linux-x64 --self-contained false -o {output dir} | Publishes the app in Release configuration. Specify --self-contained false when the .NET runtime is already installed on the server. The artifacts are written to the directory specified by -o. |
dotnet publish --self-contained true | Publishes the app with the runtime bundled. The app runs even on servers without .NET installed, but the file size will be larger. |
ASPNETCORE_ENVIRONMENT=Production | An environment variable that specifies the production environment. appsettings.Production.json is loaded automatically, allowing you to switch log levels and database connection strings to production values. |
ASPNETCORE_URLS=http://localhost:5000 | Specifies the address and port that Kestrel listens on. Restricting it to localhost is recommended so that only Nginx can reach it from the outside. |
WorkingDirectory= (systemd) | Sets the working directory for the service. Set this to the dotnet publish output directory. |
ExecStart= (systemd) | Specifies the command used to start the service. Provide either dotnet {dll name}.dll or the path to the published executable. |
Restart=always (systemd) | Automatically restarts the process if it exits unexpectedly. This setting is recommended for production services. |
proxy_pass http://localhost:5000 (Nginx) | Forwards incoming requests to Kestrel. Nginx handles SSL termination and passes requests to the backend Kestrel over HTTP. |
proxy_set_header X-Forwarded-For (Nginx) | A header that passes the client's real IP address to Kestrel. Used in combination with the UseForwardedHeaders middleware on the .NET side. |
appsettings.Production.json | A settings file that overrides appsettings.json when ASPNETCORE_ENVIRONMENT=Production is set. Use it to specify environment-specific values such as connection strings, log levels, and external API keys. |
systemctl daemon-reload | Run this after creating or editing a systemd service file. It reloads the file into systemd. |
systemctl enable {service name} | Registers the service to start automatically on OS boot. |
nginx -t | Validates the Nginx configuration file syntax. If syntax is ok is displayed, the configuration is valid. Always run this before systemctl reload nginx. |
Examples
/etc/systemd/system/kyo-dotnet.service
[Unit] # Service description, also shown in journalctl log output Description=KyoApp - Kusanagi Kyo .NET Web App (Kestrel) # Start this service after the network is available After=network.target [Service] # Service execution type. Use "simple" for foreground processes Type=simple # User and group to run the app as (avoid using root) User=kyo Group=kyo # Set the dotnet publish output directory as the working directory WorkingDirectory=/var/www/kyo-dotnet # Command to start Kestrel # Runs the published DLL with the dotnet command ExecStart=/usr/bin/dotnet /var/www/kyo-dotnet/KyoApp.dll # Explicitly set the production environment. appsettings.Production.json is loaded automatically Environment=ASPNETCORE_ENVIRONMENT=Production # Restrict Kestrel to listen on localhost only # External access is handled by Nginx Environment=ASPNETCORE_URLS=http://localhost:5000 # Automatically restart the process if it crashes Restart=always RestartSec=10 # Send standard output and errors to the systemd journal StandardOutput=journal StandardError=journal [Install] # Enable this service in multi-user.target (normal boot mode) WantedBy=multi-user.target
/etc/nginx/sites-available/kyo-dotnet.conf
server {
# Redirect HTTP (port 80) access to HTTPS
listen 80;
server_name kyo.example.com;
return 301 https://$host$request_uri;
}
server {
# Accept requests over HTTPS (port 443)
listen 443 ssl;
server_name kyo.example.com;
# Path to the SSL certificate (e.g., from Let's Encrypt)
ssl_certificate /etc/letsencrypt/live/kyo.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/kyo.example.com/privkey.pem;
location / {
# Forward requests to Kestrel (localhost:5000)
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
# Headers required to support WebSockets (e.g., SignalR)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
# Pass the hostname, client IP, and protocol to Kestrel
# These can be read via the .NET UseForwardedHeaders() middleware
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Disable buffering to improve real-time response
proxy_buffering off;
# Bypass the cache
proxy_cache_bypass $http_upgrade;
}
}
Run the following command:
$ sudo systemctl daemon-reload
$ sudo systemctl enable kyo-dotnet.service
Created symlink /etc/systemd/system/multi-user.target.wants/kyo-dotnet.service → /etc/systemd/system/kyo-dotnet.service.
$ sudo systemctl start kyo-dotnet.service
$ sudo systemctl status kyo-dotnet.service
● kyo-dotnet.service - KyoApp - Kusanagi Kyo .NET Web App (Kestrel)
Loaded: loaded (/etc/systemd/system/kyo-dotnet.service; enabled; vendor preset: enabled)
Active: active (running) since Wed 2026-03-25 10:00:00 JST; 3s ago
Main PID: 12480 (dotnet)
Tasks: 22 (limit: 4915)
Memory: 68.5M
CPU: 1.241s
CGroup: /system.slice/kyo-dotnet.service
└─12480 /usr/bin/dotnet /var/www/kyo-dotnet/KyoApp.dll
Mar 25 10:00:00 server kyo-dotnet[12480]: info: Microsoft.Hosting.Lifetime[14]
Mar 25 10:00:00 server kyo-dotnet[12480]: Now listening on: http://localhost:5000
Mar 25 10:00:00 server kyo-dotnet[12480]: Application started. Press Ctrl+C to shut down.
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo systemctl reload nginx
$ curl -I http://localhost:5000
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Server: Kestrel
Date: Wed, 25 Mar 2026 01:00:05 GMT
Overview
The standard approach to deploying C# / .NET apps in production is to generate a release build with dotnet publish and register Kestrel as a systemd unit. Kestrel is a high-performance built-in web server, but it is best practice to delegate SSL termination, static file serving, and load balancing to an Nginx reverse proxy. Setting ASPNETCORE_ENVIRONMENT=Production in the systemd Environment= directive causes appsettings.Production.json to be loaded automatically, switching connection strings and log levels to production values. Combining proxy_set_header X-Forwarded-For on the Nginx side with app.UseForwardedHeaders() on the .NET side allows the app to correctly retrieve the client's real IP address even when requests pass through Nginx. After deploying, use systemctl status and journalctl -u kyo-dotnet.service -f to monitor logs in real time for smooth troubleshooting.
If you find any errors or copyright issues, please contact us.