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.

MethodHow it worksBest for
Run from packageZip is mounted as read-only file systemMost .NET, Node, Python apps
Zip deployZip is uploaded and extracted in-placeApps that need writable wwwroot
Container deployDocker image pulled from registryContainerized apps
Web Deploy (MSDeploy)MSBuild artifact with delta syncLegacy 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
        }
      ]
Tip

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

  1. 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.
  2. 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.
  3. 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.

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.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.