# How to Publish and Deploy an ASP.NET Core App — Practical Guide

This article walks through the typical lifecycle of publishing an ASP.NET Core application and deploying it to common targets (IIS, Azure App Service, Linux systemd + Nginx, and Docker). It covers build/publish options, configuration and secrets, databases and migrations, CI/CD tips, and troubleshooting. Examples use the dotnet CLI, Visual Studio, and GitHub Actions.

* * *

## 1\. Quick overview: build vs publish vs deploy

*   Build (`dotnet build`) compiles the project for development or CI but leaves assets in intermediate folders.
    
*   Publish (`dotnet publish`) produces the final files required to run the app (DLLs, static files, runtime if self-contained, configuration).
    
*   Deploy is the process of transferring the published output to a host and configuring it to run (IIS, App Service, container registry, VM).
    

* * *

## 2\. Prepare the app for production

*   Set `ASPNETCORE_ENVIRONMENT=Production` on the host.
    
*   Use `appsettings.Production.json` to override production settings (connection strings, logging).
    
*   Do not store secrets in source code or appsettings.json. Use environment variables, Azure Key Vault, or a secrets manager.
    
*   Enable structured logging (e.g., Serilog) and external sinks for production logs.
    
*   Add health checks (`Microsoft.AspNetCore.Diagnostics.HealthChecks`) for readiness and liveness endpoints.
    
*   Ensure static files are served with caching headers and compression enabled.
    

* * *

## 3\. Publishing options

### 3.1 Using dotnet CLI

Basic publish:

```bash
dotnet publish -c Release -o ./publish
```

Publish for a specific runtime (framework-dependent; smaller):

```bash
dotnet publish -c Release -r linux-x64 --self-contained false -o ./publish
```

Self-contained deployment (includes runtime; larger, helpful where .NET runtime is not installed):

```bash
dotnet publish -c Release -r win-x64 --self-contained true -o ./publish
```

Trim unused assemblies (smaller output; test thoroughly):

```bash
dotnet publish -c Release -r linux-x64 -p:PublishTrimmed=true -o ./publish
```

Produce single-file executable (available in supported SDKs and RIDs):

```bash
dotnet publish -c Release -r linux-x64 -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ./publish
```

### 3.2 Using Visual Studio

*   Right-click project → Publish.
    
*   Create a publish target (Folder, Azure, IIS, Container Registry).
    
*   Configure settings (configuration, runtime, self-contained, target framework).
    
*   Save or use publish profiles (`.publishsettings` / `.pubxml`) for repeated deployments.
    

### 3.3 VS Code

*   Use tasks or the .NET Core CLI as above.
    
*   Consider extensions like “MSBuild Project Tools” and “Docker” for container scenarios.
    

* * *

## 4\. Deployment targets and specifics

### 4.1 Azure App Service

*   Common path: ZIP deploy, FTP, or the built-in deployment from GitHub/ZIP.
    
*   App Service can run framework-dependent apps if runtime exists; otherwise configure as self-contained.
    
*   Set app settings and connection strings in the portal (they map to environment variables).
    
*   Use deployment slots for zero-downtime swaps (staging → production).
    
*   For GitHub Actions, use `azure/webapps-deploy` action to push artifacts.
    

Example GitHub Actions step (build + deploy):

```yaml
- name: Build
  run: dotnet publish -c Release -o ./publish

- name: Deploy to Azure WebApp
  uses: azure/webapps-deploy@v2
  with:
    app-name: my-webapp
    slot-name: production
    publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
    package: ./publish
```

### 4.2 IIS (Windows)

*   Install the .NET Hosting Bundle on the server (contains ASP.NET Core Module).
    
*   Use `dotnet publish` to a folder and copy to the IIS site physical path, or use Web Deploy (MSDeploy).
    
*   The ASP.NET Core Module launches Kestrel and proxies requests to it. Configure processPath and arguments in web.config if needed (this is handled automatically by publish).
    
*   Set `ANCM` stdoutLogEnabled temporarily for troubleshooting (remember to disable in production).
    

### 4.3 Linux (systemd + reverse proxy like Nginx)

*   Publish as framework-dependent or self-contained to the server.
    
*   Create a systemd service file to run the app as a service:
    

```ini
[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
SyslogIdentifier=myapp
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://localhost:5000

[Install]
WantedBy=multi-user.target
```

*   Configure Nginx as a reverse proxy to `http://localhost:5000`, handle HTTPS, and forward headers:
    

```nginx
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;
```

*   Ensure Kestrel listens on loopback only when behind a reverse proxy.
    

### 4.4 Docker / Containers

Dockerfile (recommended multi-stage build):

```dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "MyApp.dll"]
```

*   Use container registries (Docker Hub, Azure Container Registry, GHCR).
    
*   Orchestrate with Kubernetes for scale and resiliency.
    

* * *

## 5\. Configuration, secrets, and connection strings

*   Prefer environment variables or platform-managed secrets (App Service settings, Azure Key Vault).
    
*   Map configuration using the default providers: appsettings.json → appsettings.{Environment}.json → environment variables → command-line args.
    
*   For EF Core migrations:
    
    *   Apply migrations at deploy time using `dotnet ef database update` or run migrations on app startup (make sure to handle concurrency and downtime).
        
    *   Consider running migrations from CI/CD pipeline with appropriate DB credentials and rollback plan.
        

Example: run migrations at startup (use cautiously):

```csharp
using (var scope = host.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
    db.Database.Migrate();
}
```

* * *

## 6\. CI/CD best practices

*   Build artifacts in CI (publish to artifact folder). Deploy artifacts rather than building on target servers.
    
*   Keep secrets in pipeline secret variables or secret stores.
    
*   Use deployment slots and health checks to enable safe swaps.
    
*   Run tests (unit and integration) during CI before deploying.
    
*   Use auditing and deployment logs (App Service deployment logs, container image tags).
    

Example pipeline flow:

1.  lint & test
    
2.  dotnet publish → artifact
    
3.  push artifact/container image to registry/artifacts
    
4.  deployment step to target (App Service, Kubernetes, VM)
    
5.  smoke tests & health checks
    
6.  rollback on failure
    

* * *

## 7\. Security and production hardening

*   Use HTTPS and HSTS. Terminate TLS at the edge (load balancer/Nginx) or let Kestrel handle TLS if you manage certificates.
    
*   Validate and sanitize inputs, use CSP and security headers.
    
*   Keep dependencies and runtime patched.
    
*   Limit privileges of the service account running the app.
    
*   Use connection string encryption if required and rotate secrets regularly.
    
*   Enable Application Insights / Datadog / Prometheus for observability.
    

* * *

## 8\. Performance and scaling

*   Use response compression, static file caching, and CDN for static assets.
    
*   Offload session state to distributed caches (Redis) if you scale horizontally.
    
*   Configure Kestrel limits appropriately (request body size, connection limits).
    
*   For high throughput, tune thread pool and connection settings, and scale out using load balancers or K8s.
    

* * *

## 9\. Troubleshooting checklist

*   App fails to start: check service logs (`journalctl -u myapp.service` on Linux, Event Viewer or stdout log for IIS).
    
*   Port already in use: ensure only one service binds to the configured port; behind a reverse proxy prefer loopback.
    
*   502/502.5 from IIS/App Service: check ANCM logs, and ensure the correct runtime is present.
    
*   Missing runtime errors: deploy a self-contained build or install the matching runtime on the host.
    
*   Environment mismatch: confirm `ASPNETCORE_ENVIRONMENT` and appsettings files are correct.
    
*   Connection string failures: verify secrets and network/firewall access to DB.
    

* * *

## 10\. Example: Minimal publish + systemd deploy workflow

1.  CI job: `dotnet publish -c Release -o ./artifacts`
    
2.  Copy `./artifacts` to server (scp/rsync)
    
3.  On server:
    
    *   Stop service: `sudo systemctl stop myapp`
        
    *   Replace files: `rsync -a ./artifacts/ /var/www/myapp/`
        
    *   Restart: `sudo systemctl daemon-reload && sudo systemctl restart myapp`
        
    *   Check: `sudo journalctl -u myapp -f`
        

* * *

## Summary checklist before going live

*   \[ \] Publish in Release configuration.
    
*   \[ \] Use appropriate runtime (framework-dependent vs self-contained).
    
*   \[ \] Externalize secrets and connection strings; never commit them.
    
*   \[ \] Enable structured logging and monitoring.
    
*   \[ \] Have automated CI/CD with artifact storage and rollback strategy.
    
*   \[ \] Use HTTPS, HSTS, and secure headers.
    
*   \[ \] Plan for migrations, backups, and scaling.
    
*   \[ \] Test deployment in staging and use deployment slots where available.
