.NET on Linux and macOS
.NET (formerly .NET Core) has been cross-platform by design since .NET Core 1.0, running natively on Linux distributions such as Ubuntu, Debian, RHEL, and Alpine, as well as on macOS across both Intel and Apple Silicon hardware, without emulation. The CLR, JIT compiler, and garbage collector are each compiled specifically for the target OS and CPU architecture, while the Base Class Library (BCL) exposes a consistent, normalized API surface (System.IO, System.Diagnostics.Process, and so on) so application code written once behaves the same regardless of which platform it ultimately runs on.
Cricket analogy: Just as Virat Kohli adjusts his batting technique for the bounce of the WACA in Perth versus the low, slow pitch at Chepauk in Chennai while still following the same coaching manual, the CLR's JIT compiler generates different native machine code for x64 Linux versus ARM64 macOS while executing the exact same IL.
Installing the SDK and Runtime
Installing the SDK differs by platform. On Linux, you typically add Microsoft's package feed and install via apt, yum, or zypper, or use the dotnet-install.sh script when you need a specific version without root access, common in CI pipelines and containers. On macOS, you can use the official .pkg installer from dotnet.microsoft.com or install via Homebrew with brew install dotnet. Multiple SDK versions can coexist side by side on the same machine, and a global.json file pins a project to an exact SDK version so builds stay reproducible regardless of what else is installed globally.
Cricket analogy: Just as the BCCI trains fast bowlers at the NCA in Bengaluru using regional academies while the England pathway runs through the ECB's county system, dotnet installs via apt on Ubuntu or Homebrew on macOS, both funneling into the same certified SDK.
Cross-Platform File System and Path Differences
Linux uses a case-sensitive filesystem by default, so hardcoded paths like Models\\User.cs that happen to resolve on Windows can silently fail on Linux, where Models and models are treated as different directories; a default macOS APFS volume is case-insensitive but case-preserving, which can mask the same bug locally only to have it resurface when deployed to a Linux container. Portable code should always use Path.Combine and Path.DirectorySeparatorChar instead of hardcoded separators, and cross-platform apps should prefer Environment.GetFolderPath(Environment.SpecialFolder...) over reading raw HOME/USERPROFILE environment variables directly.
Cricket analogy: Umpires reviewing a run-out with DRS must check the exact millimeter of the bat versus the crease line, since a tiny discrepancy changes the decision, much like Linux's case-sensitive filesystem treats 'User.cs' and 'user.cs' as entirely different files where Windows would silently match either.
# Linux: install via Microsoft's apt feed
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
sudo apt-get update && sudo apt-get install -y dotnet-sdk-8.0
# macOS: install via Homebrew
brew install dotnet
# systemd unit file: /etc/systemd/system/myapp.service
[Unit]
Description=My ASP.NET Core App
[Service]
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/dotnet /var/www/myapp/MyApp.dll
Restart=always
RestartSec=10
KillSignal=SIGTERM
SyslogIdentifier=myapp
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.targetProcess Model, Signals, and Systemd Integration
In production, a published .NET application on Linux is commonly run as a systemd service, with a unit file specifying WorkingDirectory, an ExecStart pointing at the dotnet executable (or a self-contained binary), and Restart=always for resiliency against crashes. Kestrel, the built-in web server, binds directly to a port and is typically placed behind a reverse proxy like nginx for TLS termination, request buffering, and additional hardening. macOS, by contrast, is used almost exclusively for local development; Apple does not position macOS as a supported server operating system for production hosting.
Cricket analogy: A team captain like Rohit Sharma delegates specific field roles to each player and trusts the support staff to manage substitutions, similar to how systemd manages the lifecycle of the Kestrel process, restarting it on crash, while nginx in front handles the 'fielding' of incoming HTTP traffic.
Handle graceful shutdown by hooking into IHostApplicationLifetime.ApplicationStopping, which fires when systemd sends SIGTERM. This gives your app a chance to drain in-flight requests before the process is killed.
A path or file reference that works fine in local Windows development can throw FileNotFoundException the moment it's deployed to a Linux container, purely because of case mismatches invisible on Windows's case-insensitive filesystem. Always test path handling on Linux before shipping.
- .NET runs natively on Linux and macOS via platform-specific CLR/JIT/GC builds, not emulation.
- Install the SDK via apt/yum on Linux, pkg installer or Homebrew on macOS, or dotnet-install.sh for scripted/CI installs.
- global.json pins a project to an exact SDK version independent of what's installed globally.
- Linux filesystems are case-sensitive; always use Path.Combine instead of hardcoded separators.
- Prefer Environment.GetFolderPath over raw HOME/USERPROFILE environment variable lookups for portability.
- Production Linux deployments typically run under systemd with Kestrel behind an nginx reverse proxy.
- macOS is a development platform for .NET, not a supported production server OS.
Practice what you learned
1. Why can a hardcoded path like `Models\\User.cs` fail on Linux even though it works on Windows?
2. What is the purpose of a global.json file?
3. In a typical Linux production deployment of an ASP.NET Core app, what role does nginx usually play?
4. Which installation method is best suited for installing a specific .NET SDK version in a CI pipeline without root access?
5. What does systemd send to a .NET service process to request graceful shutdown, and how can the app respond?
Was this page helpful?
You May Also Like
Runtime Identifiers and Native AOT
How Runtime Identifiers (RIDs) target specific platforms and how Native AOT compiles .NET apps straight to native code for fast, lightweight startup.
Publishing Single-File Apps
How .NET's single-file publishing bundles an app and its dependencies into one executable, how extraction and trimming work, and what caveats to expect.
dotnet watch and Hot Reload
How dotnet watch automatically rebuilds your app on file changes, and how Hot Reload patches a running process live to speed up the edit-test loop.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics