Deploy .NET Applications on Ubuntu: Complete Guide (English)

🌏 閱讀中文版本

This article provides a comprehensive guide on how to compile, publish, and run a .NET web application on Ubuntu 22.04.4 LTS using .NET SDK 6.0+, and manage it with systemd services.

Why Deploy .NET Applications on Ubuntu?

1. Cross-Platform Advantages

.NET Core and .NET 5+ provide full cross-platform support, allowing developers to:

  • Reduce Licensing Costs: Use free Linux servers instead of Windows Server
  • Improve Performance: .NET applications on Linux often have better memory management and execution efficiency
  • Container-Friendly: Easier integration with Docker, Kubernetes, and other modern deployment tools

2. Ubuntu’s Advantages

  • Long-Term Support (LTS): Ubuntu 22.04 LTS provides security updates until 2027
  • Rich Package Repository: apt package management provides complete dependency management
  • Community Support: Extensive documentation and community resources

3. Production Environment Considerations

  • Systemd Management: Auto-restart, log management, boot startup
  • Reverse Proxy Integration: Works with Nginx, Apache for HTTPS, load balancing
  • Monitoring & Logging: Integrates with journalctl, Prometheus, Grafana, and other tools

Web Server Architecture Explained

.NET Application Web Server Architecture

Important Concept: .NET applications use Kestrel as the built-in cross-platform web server, not IIS, and not Nginx. Here’s an explanation of each web server’s role:

Kestrel vs IIS vs Nginx Comparison

Web ServerRolePlatformDescription
Kestrel Application Server Cross-platform
(Windows, Linux, macOS)
Built-in high-performance HTTP server in .NET
Directly runs .NET applications
IIS Reverse Proxy + App Server Windows Server only Windows-specific web server
Can act as reverse proxy for Kestrel on Windows
Nginx Reverse Proxy Cross-platform
(Primarily Linux)
Does not directly run .NET apps
Forwards requests to Kestrel
Apache Reverse Proxy Cross-platform Does not directly run .NET apps
Forwards requests to Kestrel

Deployment Architecture Diagram

Typical deployment architecture on Ubuntu:

Client Request (HTTPS:443)
    ↓
Nginx (Reverse Proxy)
    ├─ SSL/TLS termination
    ├─ Static file serving (optional)
    ├─ Compression, caching
    └─ Load balancing (multiple Kestrel instances)
    ↓
Kestrel (HTTP:8080, 8081, 8082...)
    └─ Runs .NET application

Why Use Nginx as Reverse Proxy?

While Kestrel can handle HTTP requests directly, it’s recommended to use Nginx as a frontend reverse proxy in production for several reasons:

  1. SSL/TLS Termination: Nginx handles HTTPS encryption/decryption, reducing Kestrel’s workload
  2. Static File Serving: Nginx serves static files (images, CSS, JS) more efficiently
  3. Load Balancing: Distribute traffic across multiple Kestrel instances
  4. Security Protection:
    • Hide internal application structure
    • Prevent direct attacks on Kestrel
    • Implement rate limiting and DDoS protection
  5. Advanced Features:
    • Request/response compression (gzip, brotli)
    • HTTP/2 support
    • Caching strategies
    • Logging and monitoring

Windows vs Linux Deployment Differences

PlatformApp ServerReverse ProxyManagement
Windows Server Kestrel IIS (recommended)
or Nginx
Windows Service
or IIS Manager
Ubuntu/Linux Kestrel Nginx (recommended)
or Apache
systemd

This Article’s Focus: On Ubuntu, we use Kestrel (built into .NET) + Nginx (reverse proxy) + systemd (service management).


Environment Requirements

Test Environment:

  • OS: Ubuntu 22.04.4 LTS
  • .NET SDK: 6.0.128 (or newer)
  • User Permissions: sudo access required

Installing .NET SDK:

# Add Microsoft package signing key
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

# Install .NET SDK
sudo apt-get update
sudo apt-get install -y dotnet-sdk-6.0

# Verify installation
dotnet --version

Step 1: Navigate to Project Directory

Navigate to your project directory and ensure it contains a .sln or .csproj file:

cd /path/to/your/project

# Confirm project files exist
ls -la *.sln *.csproj

Step 2: Restore NuGet Packages

Restore all required NuGet packages for the project:

dotnet restore

# Example output:
# Determining projects to restore...
# Restored /path/to/project.csproj (in 2.3 sec).

Common Issues: If encountering NuGet source errors, clear the cache:

dotnet nuget locals all --clear
dotnet restore --force

Step 3: Build the Project

Build the project to ensure all source code compiles without errors:

dotnet build

# Or specify Release configuration
dotnet build -c Release

Successful Build Output:

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:15.23

Step 4: Publish the Project

Package the project to a specified output directory using Release configuration for optimal performance:

# Basic publish
dotnet publish -c Release -o /path/to/output/folder

# Self-contained publish (includes .NET Runtime)
dotnet publish -c Release -r linux-x64 --self-contained true -o /path/to/output/folder

# Single-file publish (.NET 6+)
dotnet publish -c Release -r linux-x64 --self-contained true \
  -p:PublishSingleFile=true -o /path/to/output/folder

Publish Options Explained:

ParameterDescriptionUse Case
-c ReleaseCompile with Release configurationRequired for production
-r linux-x64Specify target runtimeCross-platform publishing
–self-contained trueInclude .NET RuntimeServer without .NET SDK
PublishSingleFile=trueSingle executableSimplified deployment

Step 5: Backup Published Files

Create a backup mechanism for rollback capability:

# Create backup directory
mkdir -p ~/backupweb

# Archive publish directory with timestamp
cd ~
tar -czvf backupweb/publish_$(date +%Y%m%d_%H%M%S).tar.gz /path/to/output/folder

# Verify backup
ls -lh ~/backupweb/

Automated Backup Script:

#!/bin/bash
# backup_dotnet_app.sh

BACKUP_DIR=~/backupweb
APP_DIR=/path/to/output/folder
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

mkdir -p $BACKUP_DIR
tar -czvf $BACKUP_DIR/publish_$TIMESTAMP.tar.gz $APP_DIR

# Keep backups from last 7 days only
find $BACKUP_DIR -name "publish_*.tar.gz" -mtime +7 -delete

echo "✅ Backup completed: publish_$TIMESTAMP.tar.gz"

Step 6: Run Application Manually (Testing)

Navigate to the publish directory and start the application for testing:

cd /path/to/output/folder

# Start application (Kestrel listens on all IPs, port 8080)
dotnet YourProjectName.dll --urls "http://0.0.0.0:8080"

Replace YourProjectName.dll with your actual DLL filename, for example:

dotnet MyProject.APIService.dll --urls "http://0.0.0.0:8080"

You’ll see Kestrel startup messages:

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://0.0.0.0:8080
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.

Test the Application:

# Test from another terminal
curl http://localhost:8080

# Or test specific endpoint
curl http://localhost:8080/api/health

Step 7: Manage with Systemd

Create Systemd Service File

Create a service file to manage the application lifecycle:

sudo nano /etc/systemd/system/myproject-api.service

Service File Content:

[Unit]
Description=MyProject API Service
After=network.target

[Service]
WorkingDirectory=/path/to/output/folder
ExecStart=/usr/bin/dotnet /path/to/output/folder/MyProject.APIService.dll --urls=http://0.0.0.0:8080
Restart=always
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-myproject
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

[Install]
WantedBy=multi-user.target

Service File Parameters Explained:

ParameterDescription
After=network.targetStart after network service is ready
ExecStartExecute dotnet command to start Kestrel web server
Restart=alwaysAuto-restart on abnormal termination
RestartSec=1010-second interval before restart
KillSignal=SIGINTUse Ctrl+C signal for graceful shutdown
User=www-dataRun as www-data user (security consideration)
ASPNETCORE_ENVIRONMENTSet to Production environment

Start and Enable Service

# Reload systemd configuration
sudo systemctl daemon-reload

# Start service
sudo systemctl start myproject-api

# Enable auto-start on boot
sudo systemctl enable myproject-api

# Check service status
sudo systemctl status myproject-api

Successful Startup Output:

● myproject-api.service - MyProject API Service
     Loaded: loaded (/etc/systemd/system/myproject-api.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2024-06-22 10:00:00 UTC; 5s ago
   Main PID: 12345 (dotnet)
      Tasks: 15 (limit: 4915)
     Memory: 45.2M
        CPU: 1.234s
     CGroup: /system.slice/myproject-api.service
             └─12345 /usr/bin/dotnet /path/to/output/folder/MyProject.APIService.dll

Step 8: Troubleshooting Common Issues

1. Check Application Logs

Use journalctl to view application logs:

# Monitor logs in real-time
sudo journalctl -u myproject-api.service -f

# View last 100 lines
sudo journalctl -u myproject-api.service -n 100

# View today's logs
sudo journalctl -u myproject-api.service --since today

# View specific time range
sudo journalctl -u myproject-api.service --since "2024-06-22 10:00:00" --until "2024-06-22 11:00:00"

2. Check if Port is Listening

# Using netstat
sudo netstat -tuln | grep 8080

# Using ss (newer tool)
sudo ss -tuln | grep 8080

# Using lsof
sudo lsof -i :8080

Expected Output:

tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN

3. Check Firewall Configuration

Ensure firewall allows traffic on port 8080:

# Check ufw status
sudo ufw status

# Allow port 8080
sudo ufw allow 8080/tcp

# Or allow from specific IP range
sudo ufw allow from 192.168.1.0/24 to any port 8080

# Reload firewall
sudo ufw reload

4. Check Permission Issues

# Check file ownership
ls -la /path/to/output/folder

# Change owner to www-data
sudo chown -R www-data:www-data /path/to/output/folder

# Ensure execute permission
sudo chmod +x /path/to/output/folder/MyProject.APIService.dll

5. Check Application Configuration

Verify appsettings.json and environment variables are correctly configured:

# View environment variables
sudo systemctl show myproject-api -p Environment

# Test configuration file
cat /path/to/output/folder/appsettings.Production.json

Common Questions and Answers (FAQ)

Q1: How to Update a Deployed Application?

A: Follow these steps for zero-downtime updates:

# 1. Stop service
sudo systemctl stop myproject-api

# 2. Backup current version
tar -czvf ~/backupweb/backup_before_update_$(date +%Y%m%d).tar.gz /path/to/output/folder

# 3. Publish new version (overwrite old files)
dotnet publish -c Release -o /path/to/output/folder

# 4. Restart service
sudo systemctl start myproject-api

# 5. Check status
sudo systemctl status myproject-api

Q2: How to Configure Reverse Proxy (Nginx)?

A: Nginx acts as a reverse proxy, forwarding HTTPS requests to Kestrel. Install and configure Nginx:

# Install Nginx
sudo apt-get install nginx

# Create site configuration
sudo nano /etc/nginx/sites-available/myproject

Nginx Configuration Example (forwarding requests to Kestrel):

server {
    listen 80;
    server_name example.com;

    location / {
        # Forward requests to Kestrel (localhost:8080)
        proxy_pass http://localhost:8080;
        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;
    }
}

Explanation:

  • proxy_pass http://localhost:8080: Forwards requests to Kestrel running locally
  • Nginx listens on port 80 (HTTP) or 443 (HTTPS)
  • Kestrel runs in the background on port 8080, not directly exposed externally
# Enable site
sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/

# Test configuration
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

Q3: How to Configure HTTPS (SSL/TLS)?

A: Use Let’s Encrypt free SSL certificates configured on Nginx:

# Install Certbot
sudo apt-get install certbot python3-certbot-nginx

# Auto-configure SSL (Certbot automatically modifies Nginx config)
sudo certbot --nginx -d example.com

# Test auto-renewal
sudo certbot renew --dry-run

Flow after configuration:

Client --HTTPS(443)--> Nginx --HTTP(8080)--> Kestrel

Q4: How to Monitor Application Performance?

A: Several tools available:

  • Built-in Tools: Use dotnet-counters to monitor real-time metrics
  • APM Tools: Application Insights, New Relic, Datadog
  • Open Source: Prometheus + Grafana
# Install dotnet-counters
dotnet tool install --global dotnet-counters

# Find application PID
ps aux | grep dotnet

# Monitor metrics
dotnet-counters monitor -p [PID]

Q5: Application Stops Immediately After Starting?

A: Common causes and solutions:

  • Check Logs: sudo journalctl -u myproject-api.service -n 50
  • Port Conflict: Ensure port 8080 is not already in use
  • Permission Issues: Verify www-data user has read permissions
  • Configuration Errors: Check appsettings.json format is valid
  • Missing Dependencies: Ensure all required NuGet packages are included

Q6: How to Set Up Multiple Environments (Dev, Staging, Production)?

A: Create separate service files for each environment:

# Production environment
/etc/systemd/system/myproject-api-prod.service
Environment=ASPNETCORE_ENVIRONMENT=Production

# Staging environment
/etc/systemd/system/myproject-api-staging.service
Environment=ASPNETCORE_ENVIRONMENT=Staging
ExecStart=/usr/bin/dotnet /path/staging/MyProject.APIService.dll --urls=http://0.0.0.0:8081

# Development environment
/etc/systemd/system/myproject-api-dev.service
Environment=ASPNETCORE_ENVIRONMENT=Development
ExecStart=/usr/bin/dotnet /path/dev/MyProject.APIService.dll --urls=http://0.0.0.0:8082

Best Practices

  1. Use Reverse Proxy: Always use Nginx or Apache as frontend for HTTPS, compression, and caching
  2. Configure Application Logging: Use Serilog or NLog to write logs to files or centralized logging systems
  3. Monitoring & Alerting: Set up notifications when systemd service fails
  4. Regular Backups: Automate backup scripts, periodically clean old backups
  5. Security Hardening:
    • Run application as non-root user
    • Limit file permissions (644 for files, 755 for directories)
    • Regularly update .NET SDK and system packages
    • Configure firewall to only open necessary ports
  6. Performance Optimization:
    • Build with Release configuration
    • Enable gzip compression (Nginx level)
    • Configure Response Caching and Output Caching
    • Use connection pooling for database connections

Summary

This article provides a comprehensive guide for deploying .NET applications on Ubuntu 22.04, covering:

  • Web Server Architecture: Kestrel (application server) + Nginx (reverse proxy)
  • Build & Publish: Using dotnet CLI for restore, build, publish
  • Systemd Management: Creating service files for auto-restart and boot startup
  • Troubleshooting: Log checking, port checking, permission checking
  • Production Configuration: Reverse proxy, HTTPS, monitoring

Key Takeaways:

  1. .NET applications use built-in Kestrel web server, no IIS required
  2. Nginx acts as reverse proxy, forwarding requests to Kestrel
  3. Always use Release configuration when publishing to production
  4. Manage services with systemd to ensure high availability
  5. Configure reverse proxy for HTTPS and load balancing
  6. Establish comprehensive logging and monitoring mechanisms
  7. Regularly backup and update applications

Properly configuring these steps ensures .NET applications run stably, securely, and efficiently in Linux environments.

Related Articles

Leave a Comment