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=Productionon the host.Use
appsettings.Production.jsonto 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-deployaction 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 publishto 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
ANCMstdoutLogEnabled 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 updateor 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:
lint & test
dotnet publish → artifact
push artifact/container image to registry/artifacts
deployment step to target (App Service, Kubernetes, VM)
smoke tests & health checks
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.serviceon 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_ENVIRONMENTand appsettings files are correct.Connection string failures: verify secrets and network/firewall access to DB.
10. Example: Minimal publish + systemd deploy workflow
CI job:
dotnet publish -c Release -o ./artifactsCopy
./artifactsto server (scp/rsync)On server:
Stop service:
sudo systemctl stop myappReplace files:
rsync -a ./artifacts/ /var/www/myapp/Restart:
sudo systemctl daemon-reload && sudo systemctl restart myappCheck:
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.