Skip to main content

Command Palette

Search for a command to run...

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

Published
7 min read
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:

dotnet publish -c Release -o ./publish

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

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):

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

Trim unused assemblies (smaller output; test thoroughly):

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

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

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):

- 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:

[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:
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):

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):

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.