Learn how to deploy ASP.NET Core to Azure App Service in 10 minutes using the Azure CLI, Visual Studio, and GitHub Actions. Start deploying today!
If you want to deploy ASP.NET Core to Azure App Service, you are looking at the fastest, most battle-tested path to getting a .NET web app into production. Azure App Service is Microsoft's fully managed hosting platform — it handles the web servers, load balancing, TLS certificates, patching, and scaling so you can focus on shipping code. In this tutorial you will take a brand-new ASP.NET Core application from dotnet new to a live public URL in about 10 minutes, using three different methods: the Azure CLI (fastest), Visual Studio (most familiar), and GitHub Actions (the way real teams do it).
More importantly, we will cover why each step matters, the configuration mistakes that cause the dreaded HTTP 500.30 startup error, and the best practices that separate a demo deployment from a production-ready one.
What Is Azure App Service and Why Use It?
Azure App Service is a Platform-as-a-Service (PaaS) offering. That distinction matters. With a virtual machine (IaaS), you own the operating system: patching, IIS or Nginx configuration, certificate renewal, and security hardening are all your problem. With App Service, Microsoft owns the platform and you own only your application code.
For ASP.NET Core developers specifically, App Service is compelling because:
- First-class .NET support — new .NET releases (including .NET 8 LTS and .NET 9) are available on the platform almost immediately, on both Windows and Linux workers.
- Free tier to start — the F1 tier costs nothing and is perfect for this tutorial; you can scale to Basic, Premium, or Isolated tiers later without redeploying.
- Built-in DevOps hooks — deployment slots, GitHub Actions integration, and easy rollbacks are part of the platform, not something you bolt on.
- Automatic HTTPS — every app gets a free
*.azurewebsites.netdomain with TLS out of the box.
Prerequisites
- The .NET SDK (8.0 or later) installed locally
- An Azure account — the free account includes App Service credits
- The Azure CLI (
az) for Method 1
Verify your tooling before you start. Nothing burns your 10 minutes faster than discovering mid-deployment that az isn't on your PATH:
dotnet --version // should print 8.0.x or later
az --version // should print azure-cli 2.60+ or later
az login // opens a browser to authenticate
Step 1: Create an ASP.NET Core App
We'll use a minimal API so the whole app fits on one screen. If you already have an app, skip ahead — everything else works the same.
// Terminal:
// dotnet new web -o HelloAzure
// cd HelloAzure
// Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => Results.Ok(new
{
Message = "Hello from Azure App Service!",
Environment = app.Environment.EnvironmentName,
ServerTimeUtc = DateTime.UtcNow
}));
app.MapGet("/health", () => Results.Ok("Healthy"));
app.Run();
Two things here are deliberate. First, we expose the environment name so we can prove the app is reading Azure configuration correctly after deployment. Second, we add a /health endpoint — App Service can ping it to detect and auto-heal unhealthy instances, which you will want in production.
Run it locally with dotnet run and confirm you get JSON back at http://localhost:5000. Deploying an app you never ran locally is the number one way to waste an afternoon debugging the wrong layer.
Method 1: Deploy ASP.NET Core to Azure App Service with the Azure CLI
This is the 10-minute path. The az webapp up command creates the resource group, the App Service plan, and the web app, then zips and deploys your code — all in one step.
// From the HelloAzure project folder:
az webapp up `
--name helloazure-yourname-2026 `
--resource-group rg-helloazure `
--location eastus `
--sku F1 `
--os-type Linux `
--runtime "DOTNETCORE:8.0"
A few important details, because the defaults will bite you if you don't understand them:
- The app name must be globally unique — it becomes your URL:
https://helloazure-yourname-2026.azurewebsites.net. If the name is taken, the command fails immediately. --sku F1is the free tier. It sleeps after 20 minutes of inactivity and has a 60-minutes-per-day CPU quota. Fine for learning; useB1orP0v3for anything real.- Prefer Linux workers for ASP.NET Core. Kestrel runs directly behind the platform's front end — no IIS/ANCM layer — which means simpler config, slightly better cold starts, and cheaper compute.
- Pin the runtime explicitly. Letting the CLI guess the runtime version is how apps mysteriously break when the default changes.
After a minute or two, the CLI prints your live URL. Open it and you should see your JSON payload with "Environment": "Production". That's it — you have deployed ASP.NET Core to Azure App Service.
What Actually Happened?
Understanding the machinery helps when things go wrong. az webapp up ran dotnet publish implicitly, zipped the output, and pushed it to the Kudu deployment engine, which extracted it into /home/site/wwwroot. App Service then launched your app with dotnet HelloAzure.dll, and the platform's front end started routing HTTPS traffic to Kestrel on the port specified by the PORT environment variable — which ASP.NET Core picks up automatically. You wrote zero configuration for any of that, and that's the point of PaaS.
Method 2: Publish from Visual Studio
If you live in Visual Studio 2022, right-click the project → Publish → Azure → Azure App Service (Linux). Sign in, click Create new, accept the defaults (or pick the free tier), and hit Publish. Visual Studio builds in Release mode, publishes, and opens your browser at the live URL.
This is genuinely convenient for solo projects and prototypes. But treat right-click publish as a learning tool, not a workflow. Deployments from a developer laptop are unrepeatable — they depend on whatever code, SDK version, and local changes happen to be on that machine. Which brings us to the method real teams use.
Method 3: Continuous Deployment with GitHub Actions
The best practice for any team (and honestly, any solo project you care about) is deploying from CI. Every push to main builds, tests, and deploys — reproducibly, with an audit trail.
In the Azure Portal, open your App Service → Deployment Center → select GitHub, authorize, and pick your repository. Azure generates a workflow file automatically, but here is a clean, modern version using OpenID Connect (no stored publish-profile secrets):
# .github/workflows/deploy.yml
name: Deploy to Azure App Service
on:
push:
branches: [ main ]
permissions:
id-token: write
contents: read
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Publish
run: dotnet publish -c Release -o ./publish
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy
uses: azure/webapps-deploy@v3
with:
app-name: helloazure-yourname-2026
package: ./publish
Why OIDC instead of a publish profile? Publish profiles are long-lived credentials sitting in your repo secrets — if they leak, an attacker can deploy code to your app until you rotate them. OIDC federated credentials are short-lived tokens issued per workflow run, scoped to your specific repo and branch. Since 2023 this has been Microsoft's recommended approach, and it takes five extra minutes to set up with az ad app create and a federated credential.
Configuration: The Part Everyone Gets Wrong
Your appsettings.json should never contain production secrets. App Service provides Application Settings (Portal → Configuration), which are injected as environment variables and override your JSON config thanks to ASP.NET Core's layered configuration system. The mapping rule trips everyone up at least once: a nested JSON key uses a double underscore in App Service.
// appsettings.json (local dev value)
// { "Smtp": { "ApiKey": "dev-key" } }
// In App Service Application Settings, set:
// Name: Smtp__ApiKey <-- double underscore, not a colon
// Value: prod-key
// Your code never changes:
var apiKey = builder.Configuration["Smtp:ApiKey"];
For connection strings and real secrets, go one step further: enable the app's managed identity and pull secrets from Azure Key Vault with a reference like @Microsoft.KeyVault(SecretUri=...). Your app then has zero stored credentials anywhere — the identity is issued by the platform itself.
Best Practices and Common Pitfalls
Best Practices
- Use deployment slots (Basic tier and up). Deploy to a
stagingslot, smoke-test it on its own URL, then swap to production. The swap is a routing change, so it's near-instant — and if something breaks, swapping back is your one-click rollback. - Turn on "Always On" for paid tiers. Without it, your app is unloaded after 20 idle minutes and the next visitor pays the cold-start cost.
- Enable Application Insights. One checkbox gives you request traces, dependency timings, and exception reports. Debugging production without it is guesswork.
- Configure a health check path. Point App Service at
/health(Portal → Health check) so the platform removes and restarts unhealthy instances automatically when you scale out. - Set
ASPNETCORE_ENVIRONMENTdeliberately. App Service defaults toProduction, which is correct — but set it explicitly on staging slots so the rightappsettings.{Environment}.jsonlayer loads.
Common Pitfalls
- HTTP 500.30 / "Application failed to start." Almost always a runtime mismatch (app targets .NET 8, App Service configured for .NET 6) or a startup exception. Check Log stream in the portal, or run
az webapp log tail— the real exception is printed there. - Deploying Debug builds.
az webapp upand the GitHub Actions workflow publish Release builds, but hand-rolled scripts often forget-c Release. Debug builds are slower and leak detailed error pages. - Hardcoding ports or URLs. Never call
app.Run("http://0.0.0.0:5000"). App Service assigns the port via environment variable, and Kestrel binds to it automatically. Fighting the platform here causes container start failures on Linux plans. - Forgetting the free tier's limits. F1 apps sleep, have no custom domain TLS, and share compute. If your demo "randomly" takes 15 seconds to respond, that's a cold start, not a bug.
- Locale and time-zone assumptions. App Service instances run in UTC. If your app displays local times, convert with
TimeZoneInforather than trustingDateTime.Now.
Conclusion: Deploy ASP.NET Core to Azure App Service the Right Way
You can deploy ASP.NET Core to Azure App Service in under 10 minutes with a single az webapp up command — and that speed is real, not marketing. But the difference between a quick demo and a production system is everything around the deployment: CI/CD instead of laptop publishes, environment variables and Key Vault instead of secrets in JSON, deployment slots instead of deploy-and-pray, and Application Insights instead of flying blind.
Key takeaways:
az webapp upis the fastest path from code to a live URL — use the free F1 tier to experiment at zero cost.- Prefer Linux App Service plans for ASP.NET Core: simpler, cheaper, and Kestrel-native.
- Move to GitHub Actions with OIDC as soon as the project matters — repeatable deployments with no long-lived credentials.
- Configuration belongs in App Service Application Settings (remember the double underscore for nested keys), and secrets belong in Key Vault via managed identity.
- When startup fails,
az webapp log tailshows you the real exception — start there, not with guesswork.
Next steps: try adding a custom domain with a free App Service managed certificate, or wire up a staging slot and experience a zero-downtime swap for yourself. Once you have deployed to Azure App Service the right way once, you will never want to FTP files to a server again.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment