dotnet publish is the command that prepares a .NET application and its dependencies for deployment.
During development we usually press F5 or type dotnet run. That runs the application with a configuration designed for coding and debugging.
For production we normally use a Release build, with the optimizations and artifacts intended for deployment.
To go to production, we need to generate a Release version: optimized and packaged. The command for this is dotnet publish.
The basic command
The simplest command you can run in your terminal is:
dotnet publish -c Release -o ./publishLet’s break down what this does:
-c Release: Compiles in Release mode. The C# compiler (Roslyn) will apply aggressive optimizations to make your code run faster.-o ./publish: Saves the output to thepublishfolder.
Inside that folder you’ll see the assemblies, dependencies, configuration files, and depending on the publication type, a native executable. That is the set of files we deploy.
Publication strategies
In modern .NET, we have two main ways to package the application. Choosing the right one depends on your server.
This is the default option and does not include the full runtime.
- Requirement: The server MUST have the .NET Runtime of the target version installed (e.g., .NET 10 if publishing for .NET 10).
- Advantage: The deployment takes up less space and several applications can use the same runtime installed on the server.
This option packs everything from the .NET Runtime into your publication folder.
- Requirement: It does not require a .NET runtime installed, although the operating system must still meet the native dependencies of the chosen platform.
- Disadvantage: The size increases significantly (100MB+ for a simple API).
- Command:
# Publish for Linux 64-bit self-contained
dotnet publish -c Release -r linux-x64 --self-contained trueKestrel
When you run your compiled API (./MiApi.exe or dotnet MiApi.dll), the one listening for HTTP requests is a web server called Kestrel.
Kestrel is a lightweight, cross-platform, and extremely fast (open source) web server that comes built into ASP.NET Core.
Kestrel can be exposed directly to the Internet or placed behind a reverse proxy. IIS, Nginx, Apache, or a load balancer can facilitate TLS termination, load distribution, access logs, and integration with existing infrastructure.
This is why the reverse proxy pattern is common, although it is not mandatory in all deployments.
Architecture with reverse proxy
Instead of exposing the application directly to the public port 80/443, we put a server or load balancer in front that receives the request and passes it to Kestrel.
- Install the .NET Core Hosting Bundle on the Windows server. This installs the Runtime and a module for IIS (
AspNetCoreModuleV2). - Create a website in IIS and point it to your
publishfolder. - The IIS module will start your
.exein the background and redirect traffic.
When you run dotnet publish, a web.config file is automatically generated. Do not delete it, it tells IIS how to start your application.
- Copy your files to the server (e.g.,
/var/www/miapi). - Create a Systemd service (a
.servicefile) so your app starts automatically if the server reboots.
[Unit]
Description=My .NET API
[Service]
WorkingDirectory=/var/www/miapi
ExecStart=/usr/bin/dotnet /var/www/miapi/MiApi.dll
Restart=always
[Install]
WantedBy=multi-user.target- Configure Nginx as a Reverse Proxy to listen on port 80 and redirect to
localhost:5000(where Kestrel listens).
Single-file publication
There is a very attractive option for console tools, workers, or specific deployments: packaging the application into a single .exe or Linux binary file.
dotnet publish -c Release -r win-x64 -p:PublishSingleFile=true --self-contained trueThe result is a single MiApi.exe file. For web APIs it can be useful, but it’s not always worth it: review size, startup, native file extraction, and compatibility with your deployment platform.
Configuration management in production
A classic mistake is publishing the appsettings.Development.json or having development connection strings in the base appsettings.json.
In production, the loading order is:
appsettings.json(Base).appsettings.Production.json(Overrides).- Environment Variables (Overrides everything above).
The best practice on servers (IIS/Linux/Docker) is NOT to touch the JSON files. Use environment variables on the server to define the connection string and secret keys.
- Windows:
set ASPNETCORE_ENVIRONMENT=Production - Linux:
export ASPNETCORE_ENVIRONMENT=Production