Azure App Service is the fastest path from .NET code to a running production URL if you're already in the Microsoft ecosystem. It handles OS patching, runtime updates, TLS certificates, and load balancing — you just deploy your app.
Prerequisites
# Install Azure CLI
winget install Microsoft.AzureCLI
# Login
az login
# Set your subscription (if you have multiple)
az account set --subscription "My Subscription"Creating an App Service from CLI
# 1. Create a resource group (logical container for related resources)
az group create \
--name myapp-rg \
--location eastus
# 2. Create an App Service Plan (defines the compute tier)
az appservice plan create \
--name myapp-plan \
--resource-group myapp-rg \
--sku B1 \
--is-linux
# 3. Create the web app targeting .NET 8
az webapp create \
--name myapp-12345 \ # Must be globally unique
--resource-group myapp-rg \
--plan myapp-plan \
--runtime "DOTNETCORE:8.0"
# Your app is now live at: https://myapp-12345.azurewebsites.netPricing Tiers
Azure App Service has tiers for every need. Linux pricing (cheaper for .NET):
| Tier | Monthly Cost | RAM | vCPU | Key Features |
|---|---|---|---|---|
| F1 Free | $0 | 1 GB | Shared | 60 CPU-min/day, no custom domain TLS |
| D1 Shared | ~$10 | 1 GB | Shared | Custom domains, no SSL |
| B1 Basic | ~$13 | 1.75 GB | 1 | Always-on, custom domain SSL |
| B2 Basic | ~$26 | 3.5 GB | 2 | — |
| B3 Basic | ~$52 | 7 GB | 4 | — |
| S1 Standard | ~$69 | 1.75 GB | 1 | Deployment slots, autoscale |
| S2 Standard | ~$138 | 3.5 GB | 2 | — |
| P1v3 Premium | ~$124 | 8 GB | 2 | Zone redundancy, better networking |
| P2v3 Premium | ~$248 | 16 GB | 4 | — |
For small production apps, B1 ($13/month) is excellent. Upgrade to S1 only when you need deployment slots or autoscaling rules. The P1v3 is the best value for high-traffic production workloads.
Deploying Your Application
Method 1: ZIP Deploy (quickest)
# Build and publish
dotnet publish -c Release -o ./publish
# Create ZIP
cd ./publish && zip -r ../app.zip . && cd ..
# Deploy
az webapp deploy \
--resource-group myapp-rg \
--name myapp-12345 \
--src-path app.zip \
--type zipMethod 2: GitHub Actions (recommended for production)
First, download the publish profile from the portal or CLI:
az webapp deployment list-publishing-profiles \
--resource-group myapp-rg \
--name myapp-12345 \
--xml > publish-profile.xmlAdd the contents as a GitHub secret named AZURE_WEBAPP_PUBLISH_PROFILE, then:
# .github/workflows/deploy.yml
name: Deploy to Azure App Service
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET 8
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
- name: Publish
run: dotnet publish -c Release -o ${{ github.workspace }}/publish
- name: Deploy to Azure
uses: azure/webapps-deploy@v3
with:
app-name: myapp-12345
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: ${{ github.workspace }}/publishMethod 3: Azure CLI from GitHub Actions (OIDC auth — no secrets)
- name: Azure Login (OIDC)
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
run: |
az webapp deploy \
--resource-group myapp-rg \
--name myapp-12345 \
--src-path ./publish \
--type zipEnvironment Variables and Configuration
Setting App Settings
App settings become environment variables in your app, overriding appsettings.json:
# Set individual values
az webapp config appsettings set \
--resource-group myapp-rg \
--name myapp-12345 \
--settings \
ASPNETCORE_ENVIRONMENT=Production \
FeatureFlags__NewCheckout=true
# Set from a JSON file
az webapp config appsettings set \
--resource-group myapp-rg \
--name myapp-12345 \
--settings @app-settings.jsonConnection Strings
Connection strings get special treatment — they're accessible via ConnectionStrings:Name in .NET configuration:
az webapp config connection-string set \
--resource-group myapp-rg \
--name myapp-12345 \
--name DefaultConnection \
--connection-string "Server=myserver.database.windows.net;..." \
--connection-string-type SQLAzure// In your app, reads from Azure App Settings first, then appsettings.json
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");Using Key Vault for Secrets
Reference secrets from Key Vault without storing them in App Settings:
# Create Key Vault
az keyvault create --name myapp-kv --resource-group myapp-rg --location eastus
# Store a secret
az keyvault secret set --vault-name myapp-kv --name DbPassword --value "s3cr3t!"
# Enable system-assigned managed identity on the web app
az webapp identity assign --resource-group myapp-rg --name myapp-12345
# Get the principal ID
principalId=$(az webapp identity show --resource-group myapp-rg --name myapp-12345 --query principalId -o tsv)
# Grant the web app read access to Key Vault secrets
az keyvault set-policy --name myapp-kv --object-id $principalId --secret-permissions get listReference secrets in App Settings using Key Vault references:
az webapp config appsettings set \
--resource-group myapp-rg \
--name myapp-12345 \
--settings "DbPassword=@Microsoft.KeyVault(VaultName=myapp-kv;SecretName=DbPassword)"Deployment Slots
Deployment slots give you staging environments and zero-downtime swaps. Available on S1+ tiers.
# Create a staging slot
az webapp deployment slot create \
--name myapp-12345 \
--resource-group myapp-rg \
--slot staging
# Deploy to staging (not production)
az webapp deploy \
--resource-group myapp-rg \
--name myapp-12345 \
--slot staging \
--src-path app.zip \
--type zip
# Test staging at: https://myapp-12345-staging.azurewebsites.net
# Swap staging to production (near-zero downtime)
az webapp deployment slot swap \
--resource-group myapp-rg \
--name myapp-12345 \
--slot staging \
--target-slot production
# Roll back (swap again)
az webapp deployment slot swap \
--resource-group myapp-rg \
--name myapp-12345 \
--slot staging \
--target-slot productionConfigure slot-specific settings (like connection strings) with the "slot setting" flag. These settings DON'T swap with the slot — the staging slot keeps its staging database connection after the swap.
# Mark a setting as slot-specific (won't swap)
az webapp config appsettings set \
--resource-group myapp-rg \
--name myapp-12345 \
--slot staging \
--slot-settings ConnectionStrings__DefaultConnection="staging-db-connection"Scaling
Manual Scaling (Basic tier)
# Scale up (change VM size)
az appservice plan update \
--name myapp-plan \
--resource-group myapp-rg \
--sku S2
# Scale out (add instances) — Basic tier only supports manual scale-out
az appservice plan update \
--name myapp-plan \
--resource-group myapp-rg \
--number-of-workers 3Auto-Scaling (Standard tier and above)
# Enable autoscale for the App Service Plan
az monitor autoscale create \
--resource-group myapp-rg \
--resource myapp-plan \
--resource-type Microsoft.Web/serverFarms \
--name myapp-autoscale \
--min-count 1 \
--max-count 5 \
--count 1
# Add scale-out rule (add instance when CPU > 70%)
az monitor autoscale rule create \
--resource-group myapp-rg \
--autoscale-name myapp-autoscale \
--condition "CpuPercentage > 70 avg 5m" \
--scale out 1
# Add scale-in rule (remove instance when CPU < 30%)
az monitor autoscale rule create \
--resource-group myapp-rg \
--autoscale-name myapp-autoscale \
--condition "CpuPercentage < 30 avg 10m" \
--scale in 1Configuring Always-On
By default, apps on the Basic tier and above spin down after 20 minutes of inactivity. Enable Always On:
az webapp config set \
--resource-group myapp-rg \
--name myapp-12345 \
--always-on trueAlways On is not available on the F1 Free tier. If you're on Free and experiencing cold starts, upgrade to at least B1.
Custom Domain and TLS
# Add a custom domain
az webapp config hostname add \
--resource-group myapp-rg \
--webapp-name myapp-12345 \
--hostname www.yourdomain.com
# Create a managed TLS certificate (free)
az webapp config ssl create \
--resource-group myapp-rg \
--name myapp-12345 \
--hostname www.yourdomain.com
# Bind the certificate
az webapp config ssl bind \
--resource-group myapp-rg \
--name myapp-12345 \
--certificate-thumbprint <thumbprint-from-previous-command> \
--ssl-type SNIMonitoring with Application Insights
# Create Application Insights
az monitor app-insights component create \
--app myapp-insights \
--resource-group myapp-rg \
--location eastus
# Get the instrumentation key
instrumentationKey=$(az monitor app-insights component show \
--app myapp-insights \
--resource-group myapp-rg \
--query instrumentationKey -o tsv)
# Set on the web app
az webapp config appsettings set \
--resource-group myapp-rg \
--name myapp-12345 \
--settings APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=$instrumentationKey"Add the NuGet package to your app:
dotnet add package Microsoft.ApplicationInsights.AspNetCore// Program.cs
builder.Services.AddApplicationInsightsTelemetry();Useful CLI Reference
# View app logs in real-time
az webapp log tail --resource-group myapp-rg --name myapp-12345
# Show current app settings
az webapp config appsettings list --resource-group myapp-rg --name myapp-12345
# Restart the app
az webapp restart --resource-group myapp-rg --name myapp-12345
# Get the app's URL
az webapp show --resource-group myapp-rg --name myapp-12345 --query defaultHostName -o tsv
# Show all running apps in a resource group
az webapp list --resource-group myapp-rg --query "[].{Name:name, State:state, URL:defaultHostName}" -o table