//JorgenHoc
← All articles
.NET Hosting8 min read

Azure App Service for .NET — Setup, Pricing & Configuration

Step-by-step guide to deploying .NET 8 apps on Azure App Service: CLI setup, pricing tiers, deployment slots, environment variables, scaling, and GitHub Actions CI/CD.

#azure#dotnet#cloud#devops

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.net

Pricing Tiers

Azure App Service has tiers for every need. Linux pricing (cheaper for .NET):

TierMonthly CostRAMvCPUKey Features
F1 Free$01 GBShared60 CPU-min/day, no custom domain TLS
D1 Shared~$101 GBSharedCustom domains, no SSL
B1 Basic~$131.75 GB1Always-on, custom domain SSL
B2 Basic~$263.5 GB2
B3 Basic~$527 GB4
S1 Standard~$691.75 GB1Deployment slots, autoscale
S2 Standard~$1383.5 GB2
P1v3 Premium~$1248 GB2Zone redundancy, better networking
P2v3 Premium~$24816 GB4
💡

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 zip

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.xml

Add 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 }}/publish

Method 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 zip

Environment 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.json

Connection 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 list

Reference 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 production
💡

Configure 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 3

Auto-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 1

Configuring 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 true
⚠️

Always 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 SNI

Monitoring 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