CI/CD Pipelines for Azure App Service
Azure App Service is the most common deployment target for web applications on Azure. Its deployment slot system is purpose-built for zero-downtime CI/CD. This page shows how to build a complete pipeline that compiles code, deploys to a staging slot, runs smoke tests, and performs a production swap — with rollback if anything goes wrong.
Deployment method comparison
App Service supports several deployment methods. The choice affects deployment speed, atomicity, and cold start behavior.
| Method | How it works | Best for |
|---|---|---|
| Run from package | Zip is mounted as read-only file system | Most .NET, Node, Python apps |
| Zip deploy | Zip is uploaded and extracted in-place | Apps that need writable wwwroot |
| Container deploy | Docker image pulled from registry | Containerized apps |
| Web Deploy (MSDeploy) | MSBuild artifact with delta sync | Legacy ASP.NET projects |
Full slot-swap pipeline
The pipeline below builds a .NET web app, deploys to the staging slot, runs a smoke test against it, then swaps staging to production. The staging slot uses the same App Service plan resources as production — it warms up and receives real traffic during the swap operation, not before.
# azure-pipelines.yml — App Service slot swap pipeline
trigger:
branches:
include:
- main
variables:
buildConfiguration: 'Release'
appServiceName: 'my-webapp'
resourceGroup: 'myapp-prod-rg'
slotName: 'staging'
azureSubscription: 'My Azure Subscription'
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
displayName: 'Build'
jobs:
- job: Build
steps:
- task: UseDotNet@2
inputs:
version: '8.x'
- task: DotNetCoreCLI@2
displayName: 'Restore and Build'
inputs:
command: 'publish'
publishWebProjects: true
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: true
- task: DotNetCoreCLI@2
displayName: 'Unit Tests'
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: '--configuration $(buildConfiguration)'
- publish: $(Build.ArtifactStagingDirectory)
artifact: webapp
- stage: Deploy_Staging_Slot
displayName: 'Deploy to Staging Slot'
dependsOn: Build
jobs:
- deployment: DeployStaging
environment: 'prod-staging-slot'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: webapp
# Update App Settings for staging slot before deploying code
- task: AzureAppServiceSettings@1
displayName: 'Configure staging slot settings'
inputs:
azureSubscription: $(azureSubscription)
appName: $(appServiceName)
resourceGroupName: $(resourceGroup)
slotName: $(slotName)
appSettings: |
[
{
"name": "ASPNETCORE_ENVIRONMENT",
"value": "Staging",
"slotSetting": true
},
{
"name": "ApplicationInsights__ConnectionString",
"value": "$(STAGING_APPINSIGHTS_CONN)",
"slotSetting": true
}
]
- task: AzureWebApp@1
displayName: 'Deploy to staging slot'
inputs:
azureSubscription: $(azureSubscription)
appType: 'webApp'
appName: $(appServiceName)
deployToSlotOrASE: true
resourceGroupName: $(resourceGroup)
slotName: $(slotName)
package: '$(Pipeline.Workspace)/webapp/**/*.zip'
deploymentMethod: 'zipDeploy'
- stage: Smoke_Test
displayName: 'Smoke Test Staging Slot'
dependsOn: Deploy_Staging_Slot
jobs:
- job: SmokeTest
steps:
- task: AzureCLI@2
displayName: 'Wait for slot to be healthy'
inputs:
azureSubscription: $(azureSubscription)
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
SLOT_URL="https://$(appServiceName)-$(slotName).azurewebsites.net"
echo "Testing staging slot at $SLOT_URL"
# Retry health check for up to 2 minutes
for i in {1..12}; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$SLOT_URL/health")
echo "Attempt $i: HTTP $STATUS"
if [ "$STATUS" = "200" ]; then
echo "Staging slot is healthy"
exit 0
fi
sleep 10
done
echo "Staging slot did not become healthy in time"
exit 1
- stage: Swap_To_Production
displayName: 'Swap to Production'
dependsOn: Smoke_Test
condition: succeeded()
jobs:
- deployment: SwapSlots
environment: 'prod'
strategy:
runOnce:
deploy:
steps:
- task: AzureAppServiceManage@0
displayName: 'Swap staging slot to production'
inputs:
azureSubscription: $(azureSubscription)
WebAppName: $(appServiceName)
ResourceGroupName: $(resourceGroup)
SourceSlot: $(slotName)
SwapWithProduction: true
Managing App Service settings in pipelines
Application settings in App Service are environment variables injected into your app at runtime. Managing them in a pipeline — rather than setting them manually in the portal — ensures settings are version-controlled and consistent across deployments.
# Update connection strings and app settings as part of deployment
- task: AzureAppServiceSettings@1
displayName: 'Set App Service configuration'
inputs:
azureSubscription: 'My Azure Subscription'
appName: 'my-webapp'
resourceGroupName: 'myapp-prod-rg'
appSettings: |
[
{
"name": "WEBSITE_RUN_FROM_PACKAGE",
"value": "1"
},
{
"name": "FEATURE_FLAGS__EnableNewCheckout",
"value": "$(ENABLE_NEW_CHECKOUT)"
}
]
connectionStrings: |
[
{
"name": "DefaultConnection",
"value": "@Microsoft.KeyVault(SecretUri=https://mykeyvault.vault.azure.net/secrets/sql-conn/)",
"type": "SQLAzure",
"slotSetting": false
}
]
Use Key Vault references in App Service Application Settings (@Microsoft.KeyVault(SecretUri=…)) rather than storing secret values directly. The App Service’s managed identity fetches the secret at runtime. This means no secret ever passes through your pipeline in plaintext, and rotating the secret in Key Vault takes effect immediately without a redeployment.
Deploying a container to App Service
- stage: Deploy_Container
jobs:
- deployment: DeployContainer
environment: 'prod'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebAppContainer@1
displayName: 'Deploy container image to App Service'
inputs:
azureSubscription: 'My Azure Subscription'
appName: 'my-webapp'
imageName: 'myappregistry.azurecr.io/myapp/api:$(Build.BuildId)'
slotName: 'staging'
Rollback with a slot re-swap
One of the main reasons to use deployment slots is how easy rollback becomes. After a swap, the previous production code is now in the staging slot. To roll back, you swap again — returning the old code to production and the new (broken) code to staging.
# Rollback: swap staging (which has the old good code) back to production
az webapp deployment slot swap \
--name my-webapp \
--resource-group myapp-prod-rg \
--slot staging \
--target-slot production
# Verify production URL is healthy after rollback
curl -I https://my-webapp.azurewebsites.net/health
Common mistakes
- Deploying configuration and code in separate steps without slot-sticky settings. Settings marked as slot-sticky stay with the slot when you swap. If your production slot has a sticky connection string and you swap staging to production, the old connection string remains on production — which is usually what you want. If a setting is not sticky, it travels with the slot, which can cause production to suddenly use staging’s database credentials after a swap.
- Not warming up the staging slot before swapping. A slot that was just deployed has cold-started JIT runtimes and empty in-memory caches. Swapping it to production immediately causes a slow first request for real users. Hit the staging slot’s health endpoint or run warm-up requests before swapping.
- Deploying directly to the production slot. Deploying code directly to the production slot causes a brief restart and potential downtime while the app initializes. Always deploy to a staging slot and swap, even if you do not run smoke tests — the swap itself is near-instant.
Summary
- App Service deployment slots enable zero-downtime deployments. Deploy to staging, verify, then swap to production in seconds.
- Run-from-package (WEBSITE_RUN_FROM_PACKAGE=1) is the recommended deployment method for most App Service scenarios — deployments are atomic and startup is faster.
- Use the AzureAppServiceSettings task in your pipeline to manage App Settings and connection strings as code rather than configuring them manually in the portal.
- Mark environment-specific settings as slot-sticky so they stay with the slot during swaps, preventing staging credentials from leaking into production.
- Rollback is a re-swap — the previous production code lives in the staging slot after the initial swap, ready to be restored in seconds.
Frequently asked questions
What is a deployment slot in App Service and why use it in CI/CD?
A deployment slot is a live version of your App Service with its own hostname. You deploy to the staging slot first, warm it up, then swap staging into production. The swap is near-instant and zero-downtime. If something goes wrong, you swap back. Standard and Premium tiers support slots.
What deployment method should I use — zip deploy, run from package, or web deploy?
Run from package (WEBSITE_RUN_FROM_PACKAGE=1) is recommended for most scenarios. The zip is mounted directly and the deployment is atomic — no partial file state. Zip deploy is similar but extracts the files. Web deploy (MSDeploy) is older and best avoided for new projects unless you have a specific reason.
How do I pass environment-specific settings to App Service from a pipeline?
Use the AzureAppServiceSettings@1 task to update App Settings and Connection Strings as part of your deployment stage. Do not bake environment-specific values into your build artifact. Keep configuration in App Service Application Settings or Key Vault references, and set them in the pipeline.