How to Deploy a .NET Application on RHEL 7

Microsoft’s .NET (formerly .NET Core) is a cross-platform, open-source runtime that runs on Linux, including Red Hat Enterprise Linux 7. With Microsoft publishing an official yum repository for RHEL, installing .NET SDK and deploying web applications is straightforward and fully supported. In this tutorial, you will configure Microsoft’s package repository, install the .NET 8.0 SDK, create and publish a web API project, configure it as a systemd service, set up Nginx as a reverse proxy to the Kestrel web server, and manage application configuration through environment variables in the systemd unit file.

Prerequisites

  • RHEL 7 with a valid subscription and access to yum repositories
  • sudo or root access
  • Nginx installed or available to install via EPEL
  • At least 2 GB of free disk space
  • Basic familiarity with systemd service management

Step 1: Configure the Microsoft .NET yum Repository

Microsoft publishes an official RPM repository at packages.microsoft.com that provides .NET SDK and runtime packages for RHEL 7. Add this repository before attempting to install any .NET packages.

# Download and install the Microsoft repository configuration RPM
sudo rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm

# Verify the repository was added
yum repolist | grep microsoft

# Check available .NET packages
yum list available | grep dotnet

If the RPM URL has changed (Microsoft periodically updates repository configuration packages), check https://packages.microsoft.com/config/rhel/7/ for the current filename. Alternatively, you can configure the repository manually:

sudo tee /etc/yum.repos.d/microsoft-prod.repo <<'EOF'
[packages-microsoft-com-prod]
name=packages-microsoft-com-prod
baseurl=https://packages.microsoft.com/yumrepos/microsoft-rhel7-prod
enabled=1
gpgcheck=1
gpgkey=https://packages.microsoft.com/keys/microsoft.asc
EOF

sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc

Step 2: Install the .NET SDK

Install the .NET 8.0 SDK, which includes the runtime, the dotnet CLI, and all build tools required to compile and publish .NET applications.

# Install the .NET 8.0 SDK
sudo yum install -y dotnet-sdk-8.0

# Verify the installation
dotnet --version
dotnet --list-sdks
dotnet --list-runtimes

You should see output like 8.0.xxx from dotnet --version. The --list-sdks and --list-runtimes commands are useful when multiple SDK versions are installed side-by-side.

If you only need to run (not build) .NET applications on a target server, install the runtime instead of the full SDK to minimize the installed footprint:

sudo yum install -y dotnet-runtime-8.0
sudo yum install -y aspnetcore-runtime-8.0

Step 3: Create a New Web API Project

Use the dotnet new scaffold to create a minimal Web API project. This generates a production-ready template with Swagger, basic routing, and Kestrel web server configuration.

mkdir -p ~/dotnet-apps
cd ~/dotnet-apps

# Create a new web API project
dotnet new webapi -n MyWebApi --no-https

cd MyWebApi

# Inspect the project structure
ls -la
cat Program.cs

The --no-https flag disables the HTTPS redirect middleware since Nginx will handle TLS termination in production. Test the application locally before publishing:

# Start the development server (binds to http://localhost:5000 by default)
dotnet run &
DEV_PID=$!

sleep 3

# Test the API endpoint
curl -s http://localhost:5000/weatherforecast | python -m json.tool

# Stop the development server
kill $DEV_PID

Step 4: Publish the Application for Production

Publishing compiles the application and collects all dependencies into a single directory that can be deployed to a server. Use the -c Release flag to enable production optimizations and -r linux-x64 to target the RHEL 7 runtime.

cd ~/dotnet-apps/MyWebApi

# Publish as a framework-dependent deployment (requires dotnet runtime on target)
dotnet publish -c Release -r linux-x64 --self-contained false -o /tmp/mywebapi-publish

# OR publish as a self-contained executable (no dotnet runtime needed on target)
dotnet publish -c Release -r linux-x64 --self-contained true 
  -p:PublishSingleFile=true 
  -p:EnableCompressionInSingleFile=true 
  -o /tmp/mywebapi-selfcontained

# Check the published output
ls -lh /tmp/mywebapi-publish/

Framework-dependent deployments are smaller and benefit from OS-level security patches to the shared runtime. Self-contained deployments are fully portable and do not require .NET to be installed on the target server — ideal for locked-down production environments.

Step 5: Copy to /opt and Create the Application User

Place the application in /opt following the Linux Filesystem Hierarchy Standard for third-party applications. Run the service under a dedicated non-privileged user account.

# Create a dedicated system user (no login shell, no home directory)
sudo useradd -r -s /sbin/nologin -d /opt/mywebapi appuser

# Create the application directory
sudo mkdir -p /opt/mywebapi
sudo cp -r /tmp/mywebapi-publish/* /opt/mywebapi/

# Set ownership
sudo chown -R appuser:appuser /opt/mywebapi

# Create log directory
sudo mkdir -p /var/log/mywebapi
sudo chown appuser:appuser /var/log/mywebapi

# Verify the main binary
ls -lh /opt/mywebapi/MyWebApi

Step 6: Create a systemd Service Unit

Configure the .NET application as a systemd service so it starts automatically on boot and restarts on failure. Environment variables defined in the unit file override appsettings.json values at runtime, following the ASP.NET Core configuration hierarchy.

sudo tee /etc/systemd/system/mywebapi.service <<'EOF'
[Unit]
Description=My .NET Web API
After=network.target
Wants=network-online.target

[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/mywebapi

# For framework-dependent: use dotnet binary
ExecStart=/usr/bin/dotnet /opt/mywebapi/MyWebApi.dll

# For self-contained binary, replace above with:
# ExecStart=/opt/mywebapi/MyWebApi

Restart=on-failure
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=mywebapi

# ASP.NET Core environment and configuration
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://127.0.0.1:5000
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

# Database connection string (override appsettings.json)
Environment=ConnectionStrings__DefaultConnection=Server=localhost;Database=mydb;User=myuser;Password=secret

# Logging
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable mywebapi.service
sudo systemctl start mywebapi.service
sudo systemctl status mywebapi.service

Using double underscores (__) in environment variable names maps to the ASP.NET Core configuration hierarchy. For example, ConnectionStrings__DefaultConnection corresponds to ConnectionStrings:DefaultConnection in appsettings.json. This allows you to override any nested configuration value via environment variables without modifying the published application files.

Step 7: Configure Nginx as a Reverse Proxy

Kestrel, the .NET web server, is designed to run behind a reverse proxy like Nginx in production. Nginx handles TLS termination, static file caching, request buffering, and connection management, while Kestrel focuses on application logic.

# Install Nginx (requires EPEL on RHEL 7)
sudo yum install -y epel-release
sudo yum install -y nginx

sudo systemctl enable nginx
sudo systemctl start nginx

Create an Nginx server block for the .NET application:

sudo tee /etc/nginx/conf.d/mywebapi.conf <<'EOF'
server {
    listen 80;
    server_name myapp.example.com;

    location / {
        proxy_pass         http://127.0.0.1:5000;
        proxy_http_version 1.1;

        # Required for WebSocket support and ASP.NET Core
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection keep-alive;
        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;

        # Forward port to allow ASP.NET Core to generate correct redirect URLs
        proxy_set_header   X-Forwarded-Port $server_port;

        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 300s;
        proxy_connect_timeout 75s;
    }

    # Serve a static error page if Kestrel is down
    error_page 502 503 /maintenance.html;
    location = /maintenance.html {
        root /usr/share/nginx/html;
        internal;
    }
}
EOF

# Test the Nginx configuration
sudo nginx -t

# Reload Nginx without downtime
sudo systemctl reload nginx

Step 8: Verify the Deployment

# Check the application is listening on port 5000
ss -tlnp | grep 5000

# Check Nginx is listening on port 80
ss -tlnp | grep :80

# Test via Nginx
curl -i http://myapp.example.com/weatherforecast

# View application logs via journald
sudo journalctl -u mywebapi.service -f

# View the last 100 lines
sudo journalctl -u mywebapi.service -n 100 --no-pager

Conclusion

You have configured Microsoft’s official yum repository on RHEL 7, installed the .NET 8.0 SDK, created and published a Web API application for Linux, and deployed it to /opt/mywebapi under a dedicated non-privileged system user. The application is managed by systemd with automatic restart on failure, and all configuration is injected via environment variables in the unit file — keeping secrets out of the application codebase. Nginx acts as the production-grade reverse proxy in front of Kestrel, handling incoming connections and forwarding necessary headers. This stack forms a solid, maintainable foundation for running .NET workloads on RHEL 7 enterprise environments.