
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Teams often build robust APIs but stumble when they deploy ASP.NET Core to production on Linux because the runtime behaves differently than in Visual Studio. A common mistake is running Kestrel directly as a public-facing server or storing secrets in appsettings.json, both of which create security and reliability risks. This guide covers the exact configuration I use to ship .NET 9 applications safely behind Nginx with full observability.
How do you configure Nginx as a reverse proxy for ASP.NET Core?
Kestrel is an excellent application server, but it lacks the battle-hardened edge features required for public traffic. When you install Nginx on Ubuntu as a reverse proxy, you gain TLS termination, HTTP/2 support, request buffering, and protection against slow-loris attacks. The key is forwarding headers correctly so your application sees the real client IP and protocol.
Nginx site configuration
Create /etc/nginx/sites-available/aspnet-app with explicit header forwarding. Never rely on default proxy settings in production.
server {
listen 80;
server_name api.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_buffering on;
proxy_read_timeout 90s;
}
location /health {
proxy_pass http://127.0.0.1:5000/health;
access_log off;
}
} Configure forwarded headers in .NET
Your application must trust the proxy and map headers to HttpContext. Add this early in Program.cs, before authentication or routing middleware:
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor
| ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Add(IPAddress.Parse("127.0.0.1"));
});
app.UseForwardedHeaders(); A common mistake is placing UseForwardedHeaders() after UseAuthentication(). This causes HTTPS redirects to loop because the auth middleware sees HTTP instead of the original scheme. Always verify header order matches the official Microsoft documentation.
How do you create a secure systemd service for ASP.NET Core?
Running your app as a background process with nohup or screen sessions is not production-grade. Systemd provides automatic restarts, resource limits, logging integration, and security sandboxing. For teams managing compliance like SOC 2, systemd unit files serve as auditable configuration artifacts that demonstrate least-privilege execution.
Hardened unit file
Create /etc/systemd/system/aspnet-app.service with security directives enabled:
[Unit]
Description=ASP.NET Core Production API
After=network.target postgresql.service
Wants=postgresql.service
[Service]
User=aspnet-app
Group=aspnet-app
WorkingDirectory=/opt/aspnet-app/current
ExecStart=/opt/aspnet-app/current/MyApp --urls=http://127.0.0.1:5000
Restart=always
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=aspnet-app
Environment=ASPNETCORE_ENVIRONMENT=Production
EnvironmentFile=/opt/aspnet-app/env.production
ProtectSystem=strict
ProtectHome=true
NoNewPrivileges=true
PrivateTmp=true
ReadWritePaths=/opt/aspnet-app/logs
[Install]
WantedBy=multi-user.target Why these security directives matter
- NoNewPrivileges=true prevents child processes from escalating privileges via setuid binaries.
- ProtectSystem=strict makes the entire filesystem read-only except paths in
ReadWritePaths. - EnvironmentFile keeps secrets out of the unit file itself, allowing separate permission management.
- SyslogIdentifier ensures logs route correctly to journald for centralized collection with tools like Graylog centralized log management.
Always create a dedicated system user (useradd -r -s /usr/sbin/nologin aspnet-app) rather than running as root or a developer account. This limits blast radius if the application is compromised.
How should you manage secrets and configuration in production?
Never commit appsettings.Production.json with real credentials to source control. Even encrypted config files add operational complexity during rotation. Environment variables remain the most portable and audit-friendly approach for injecting secrets at runtime.
Environment file structure
Store production secrets in /opt/aspnet-app/env.production with restrictive permissions:
ConnectionStrings__Default=Host=db.internal;Database=myapp;Username=app_user;Password=${DB_PASSWORD}
Jwt__SigningKey=${JWT_SIGNING_KEY}
Redis__ConnectionString=redis.internal:6379,password=${REDIS_PASS}
Serilog__WriteTo__1__Args__connectionString=${SEQ_CONNECTION} sudo chown root:aspnet-app /opt/aspnet-app/env.production
sudo chmod 640 /opt/aspnet-app/env.production The double-underscore (__) syntax maps nested JSON keys to flat environment variables, which .NET parses automatically. For cloud-native deployments, integrate with HashiCorp Vault or AWS Secrets Manager and populate this file during deployment rather than storing long-lived credentials on disk.
What deployment strategy minimizes downtime for ASP.NET Core?
Directly overwriting running binaries causes failed requests during file locks. Use atomic directory swaps with symlink redirection to achieve zero-downtime releases without complex orchestration.
Atomic deployment script
- Publish to a timestamped directory:
/opt/aspnet-app/releases/20260820-1430/ - Run smoke tests against the new release on localhost:5001 before switching traffic.
- Update the
/opt/aspnet-app/currentsymlink atomically:ln -sfn /opt/aspnet-app/releases/20260820-1430 /opt/aspnet-app/current - Reload the service gracefully:
systemctl reload-or-restart aspnet-app - Verify health endpoint returns 200 within 30 seconds; rollback symlink if not.
This approach keeps previous releases available for instant rollback. Retain at least three releases and automate cleanup via cron. For teams adopting GitOps patterns similar to those in setting up GitOps with ArgoCD, adapt this pattern to pull artifacts from your registry rather than pushing directly.
How do you compare hosting options when you deploy ASP.NET Core to production?
| Criteria | Linux VM + Nginx | Azure App Service | Kubernetes (AKS/EKS) |
|---|---|---|---|
| Setup complexity | Moderate (manual hardening) | Low (managed platform) | High (cluster ops required) |
| Cost at low scale | Low ($5–20/mo VPS) | Medium ($50+/mo minimum) | High ($100+/mo base) |
| Auto-scaling granularity | Manual or custom scripts | Built-in metric-based | HPA/VPA with fine control |
| Compliance evidence | Self-managed audit trails | Microsoft compliance docs | Full infrastructure-as-code |
| Best for | SMEs, Nepal-local hosting | Enterprise Azure shops | Microservices at scale |
For Nepali businesses targeting local users with budget constraints, a well-hardened Ubuntu VPS remains cost-effective. Global SaaS products benefit from managed platforms that reduce operational overhead. Choose based on team size, compliance requirements, and traffic predictability—not hype.
Deploy ASP.NET Core to Production with Confidence
Shipping .NET applications to Linux servers requires treating the runtime as one component in a layered defense strategy. Configure Nginx explicitly, harden systemd units, externalize secrets, and automate deployments with atomic swaps. Monitor endpoints continuously using the four golden signals of monitoring to catch regressions before users report them. If your team needs help designing compliant .NET infrastructure or auditing existing deployments, reach out to discuss your specific requirements.