Skip to main content

Use deployment slots for zero-downtime releases

In this lab, you release a new version of a web app to Azure App Service with no downtime, using a deployment slot and a swap. A deployment slot is a live copy of your app with its own hostname. You deploy and warm up the new version in a staging slot, then swap it into production in a few seconds. Because the swap only re-points traffic once the new instances are ready, your users never see a cold start or a broken deploy.

You will deploy version 1 (v1) to production, create a staging slot, deploy version 2 (v2) to staging, warm it up, and swap. Production then serves v2. If something looks wrong, you swap back to instantly roll back to v1. You also configure slot-specific ("sticky") settings that stay put during a swap, and see how swap with preview lets you validate production configuration before you commit.

You will do this three ways so you can pick the workflow that fits you:

  • Azure Developer CLI (azd) - provision the app (and slot) from infrastructure, then deploy.
  • Azure CLI (az) - explicit az webapp deployment slot commands to create, deploy, and swap.
  • Azure portal - the visual Deployment slots blade.

Slots are language-agnostic: the swap mechanics are identical whether your app is .NET, Node.js, Python, Java, or PHP. The lab uses a tiny app that prints its version so you can see the swap happen; pick your language for the app, and the slot steps stay the same.

App Service Labs complements Microsoft Learn

This lab is a hands-on, end-to-end walkthrough. For reference depth on any concept, follow the "Learn more" links to the official Microsoft Learn articles.

Estimated time: 30-45 minutes

What you'll build

A production web app on a Standard (S1) App Service plan with a staging deployment slot. You deploy v1 to production and v2 to staging, then swap staging into production with zero downtime. When you finish, curl against the production hostname returns HTTP 200 with a body that changes from Hello from v1 to Hello from v2 across the swap - and back again after you roll back.

Objectives

By the end of this lab you will be able to:

  • Explain how a slot swap gives you a zero-downtime release and an instant rollback.
  • Create a staging deployment slot on a Standard-or-higher App Service plan.
  • Deploy different versions to production and staging, then swap them.
  • Configure slot-specific (sticky) settings that do not move during a swap.
  • Use swap with preview to validate production configuration before completing the swap.
  • Roll back a bad release by swapping back.

Prerequisites

Before you begin, you will need an Azure subscription with Owner permissions and a GitHub account.

In addition, you will need the following tools installed on your local machine:

Setup Azure CLI

Start by logging into Azure by run the following command and follow the prompts:

az login --use-device-code
tip

You can log into a different tenant by passing in the --tenant flag to specify your tenant domain or tenant ID.

Slots need Standard tier or higher

Deployment slots are a feature of the Standard, Premium, and Isolated pricing tiers. The Free (F1) and Basic (B1, B2, B3) tiers have no slots - you cannot create one there. This lab uses the Standard S1 tier (Linux, about USD 70/month), the smallest tier that supports slots. Standard S1 includes up to 5 slots per app. Delete the resources when you finish (see Clean up) so you are not billed for idle time.

How a slot swap works

Each slot is a full deployment of your app with its own hostname (https://<app>-<slot>.azurewebsites.net). You deploy to the staging slot and let it warm up - App Service starts the app and, when configured, hits its warm-up path so the runtime is JIT-compiled and caches are primed.

A swap does not copy files. It swaps the two slots' running instances and their non-sticky configuration, after warming up the source slot against the target's settings. Because the staging instances are already running and warm, App Service just re-points the production hostname to them. In-flight requests finish on the old instances; new requests land on the new ones. There is no restart on the production hostname, so there is no downtime.

After the swap, the old production version (v1) lives in the staging slot. That is your rollback: swap again and production instantly serves v1 once more.

Sticky (slot) settings

Most app settings and connection strings move with the app during a swap. You can mark specific ones as a deployment slot setting ("sticky") so they stay with the slot instead. Use sticky settings for anything that must differ between staging and production - for example, a staging slot that points at a test database, or a feature flag you only turn on in production.

Set your path

Set your tooling and language once. Every matching step and code sample below follows your choice.

Choose your path
Set these once - every matching step and code sample below follows your choice.
Tooling
Language

Create the app and deploy v1

First, create a web app on a Standard S1 plan and deploy version 1. Use the tiny "prints its version" app below, or bring your own - the slot steps are the same either way.

The app is intentionally minimal: it returns a single line, Hello from v1, and reads an optional RELEASE_COLOR setting so you can see sticky settings in action. For v2, you change v1 to v2 in the response and redeploy - nothing else changes.

Create a minimal API in Program.cs:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var version = "v1"; // change to "v2" for the second release
app.MapGet("/", () =>
{
var color = Environment.GetEnvironmentVariable("RELEASE_COLOR") ?? "none";
return Results.Text($"Hello from {version} (slot color: {color})\n");
});
app.Run();

Now create the app and deploy v1. Choose your tooling.

With azd you define the app and its staging slot in infrastructure, then deploy. azd deploy targets the production slot; you deploy to staging with az in a later step.

1. Sign in

azd auth login

2. Add an azure.yaml and infrastructure

In your app folder, create azure.yaml (set language to dotnet, js, python, or java):

# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: slots-demo
services:
web:
project: ./src
language: js
host: appservice

Create infra/main.parameters.json:

{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environmentName": { "value": "${AZURE_ENV_NAME}" },
"location": { "value": "${AZURE_LOCATION}" },
"resourceGroupName": { "value": "${AZURE_RESOURCE_GROUP}" }
}
}

Create infra/main.bicep. It runs at subscription scope so azd creates and owns the resource group:

targetScope = 'subscription'

@description('Name of the azd environment; used to derive resource names.')
param environmentName string

@description('Azure region for all resources.')
param location string

@description('Resource group to create for this environment.')
param resourceGroupName string

resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
name: resourceGroupName
location: location
}

module resources 'resources.bicep' = {
name: 'resources'
scope: rg
params: {
location: location
environmentName: environmentName
}
}

output WEB_URI string = resources.outputs.webUri

Create infra/resources.bicep. The key parts are the S1 plan (slots need Standard or higher) and a child staging slot resource:

@description('Azure region for all resources.')
param location string

@description('Name of the azd environment; used to derive resource names.')
param environmentName string

var token = uniqueString(subscription().id, environmentName, location)
var appName = 'app-${token}'

resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: 'plan-${token}'
location: location
sku: {
name: 'S1' // Standard - required for deployment slots
tier: 'Standard'
}
kind: 'linux'
properties: {
reserved: true
}
}

resource web 'Microsoft.Web/sites@2023-12-01' = {
name: appName
location: location
tags: { 'azd-service-name': 'web' }
properties: {
serverFarmId: plan.id
siteConfig: {
linuxFxVersion: 'NODE|22-lts' // match your language
appCommandLine: 'node server.js'
}
httpsOnly: true
}
}

resource stagingSlot 'Microsoft.Web/sites/slots@2023-12-01' = {
parent: web
name: 'staging'
location: location
properties: {
serverFarmId: plan.id
siteConfig: {
linuxFxVersion: 'NODE|22-lts'
appCommandLine: 'node server.js'
}
httpsOnly: true
}
}

output webUri string = 'https://${web.properties.defaultHostName}'
Set linuxFxVersion for your language

Use the runtime that matches your app: DOTNETCORE|8.0, NODE|22-lts, PYTHON|3.13, JAVA|17-java17, or PHP|8.4. List valid values with az webapp list-runtimes --os linux. Remove appCommandLine for stacks that do not need a custom start command.

3. Provision and deploy v1

azd up

azd up prompts for an environment name, region, and subscription, then provisions the plan, web app, and staging slot and deploys v1 to production. Note the WEB_URI it prints.

Confirm production serves v1 before you go on:

curl https://<app-name>.azurewebsites.net

You should see Hello from v1 (slot color: none).

Create a staging slot

Add a staging slot. A new slot starts empty unless you clone configuration from another slot; here you clone from production so staging inherits the same runtime and settings to start.

Your infra/resources.bicep already declares the staging slot, so azd up created it. Confirm it exists:

az webapp deployment slot list -g <resource-group> -n <app-name> -o table
azd owns the resource group

azd created and named the resource group for your environment. Find it with azd env get-values | grep AZURE_RESOURCE_GROUP, then use that name where the commands below say <resource-group>.

The staging slot now has its own hostname: https://<app-name>-staging.azurewebsites.net.

Configure a slot-specific (sticky) setting

Sticky settings stay with a slot during a swap. Mark RELEASE_COLOR as a deployment slot setting on both slots, with a different value each, so you can watch it stay put when you swap. Production keeps blue; staging keeps green.

Use az to set sticky settings on the slots azd provisioned:

az webapp config appsettings set \
-g <resource-group> -n <app-name> \
--slot-settings RELEASE_COLOR=blue

az webapp config appsettings set \
-g <resource-group> -n <app-name> --slot staging \
--slot-settings RELEASE_COLOR=green

Deploy v2 to staging and warm it up

Now build version 2. Change the version string in your app from v1 to v2 (the one-line change called out in each language sample above), then deploy it to the staging slot only. Production keeps serving v1 the whole time.

azd deploy targets production, so deploy the v2 build to the staging slot with az. Zip your updated app and deploy to the slot:

zip -r v2.zip .
az webapp deploy \
-g <resource-group> -n <app-name> --slot staging \
--src-path v2.zip --type zip

Confirm staging serves v2 while production still serves v1:

echo -n "PROD: "; curl -s https://<app-name>.azurewebsites.net
echo -n "STAGING: "; curl -s https://<app-name>-staging.azurewebsites.net

Expected (validated during authoring):

PROD: Hello from v1 (slot color: blue)
STAGING: Hello from v2 (slot color: green)
Warm up before you swap

Hit the staging hostname a few times, or set the WEBSITE_SWAP_WARMUP_PING_PATH and WEBSITE_SWAP_WARMUP_PING_STATUSES app settings so App Service pings a path and waits for a healthy status before completing a swap. Warming up first is what keeps the swap instantaneous for your users. See Specify custom warm-up.

Swap staging into production

Swap the staging slot into production. App Service warms up the staging instances against production settings, then re-points the production hostname to them.

Swap with az (there is no azd swap command):

az webapp deployment slot swap \
-g <resource-group> -n <app-name> \
--slot staging --target-slot production
Swap with preview

For a safer swap, use swap with preview (a multi-phase swap). Phase 1 applies production's settings to the staging instances and warms them up, but leaves the production hostname on the old version, so you can test the staging hostname with production configuration. If it looks good, complete the swap; if not, cancel and nothing changes for users. With the CLI, start with az webapp deployment slot swap ... --action preview and finish with --action swap. See Swap with preview.

Verify

Confirm production now serves v2 and returns HTTP 200:

curl -i https://<app-name>.azurewebsites.net

Expected output (validated during authoring):

HTTP/1.1 200 OK
Content-Type: text/plain

Hello from v2 (slot color: blue)

Two things to notice:

  • The body changed from Hello from v1 to Hello from v2 with no failed requests in between - a zero-downtime release.
  • The slot color is still blue, not green. The sticky RELEASE_COLOR setting stayed with the production slot, exactly as intended.

The old version (v1) now lives in the staging slot - that is your rollback source. The sticky settings are authoritative in configuration: production keeps blue and staging keeps green, whichever build each currently holds. Confirm it directly:

az webapp config appsettings list -g <resource-group> -n <app-name> \
--query "[?name=='RELEASE_COLOR'].{name:name,value:value,sticky:slotSetting}" -o table
az webapp config appsettings list -g <resource-group> -n <app-name> --slot staging \
--query "[?name=='RELEASE_COLOR'].{name:name,value:value,sticky:slotSetting}" -o table
Name Value Sticky
------------- ------- --------
RELEASE_COLOR blue True

Name Value Sticky
------------- ------- --------
RELEASE_COLOR green True
Slot values settle after a restart

Right after a swap, the staging slot can briefly still report the color it was warmed up with, because App Service warms the source slot against the target's configuration during the swap. Its own sticky value applies once its instances restart. The production hostname - the one that matters for users - reflects its sticky value immediately, as you saw above.

Roll back with a swap back

Because v1 is now in the staging slot, rolling back is just another swap. Run the swap again and production instantly returns to v1:

az webapp deployment slot swap \
-g <resource-group> -n <app-name> \
--slot staging --target-slot production

Confirm production is back on v1:

curl -s https://<app-name>.azurewebsites.net
Hello from v1 (slot color: blue)

That instant rollback is the biggest operational win of slots: a bad release is one swap away from being undone, with no rebuild and no redeploy.

Clean up resources

To avoid ongoing charges, delete the resources when you are done. Deleting the resource group removes the web app, its slots, and the App Service plan.

azd down --purge --force

Summary

In this lab, you released a new version of a web app with no downtime by using a deployment slot and a swap, and confirmed production returned HTTP 200. You learned how to:

  • Create a staging deployment slot on a Standard (S1) plan, and why Free and Basic tiers have no slots.
  • Deploy different versions to production and staging, then swap to release v2 with zero downtime.
  • Configure slot-specific (sticky) settings that stay put during a swap.
  • Use swap with preview to validate production configuration before you commit.
  • Roll back instantly by swapping back to the previous version.
  • Do all of this three ways - with azd, the Azure CLI, and the portal's Deployment slots blade.

Troubleshooting

az webapp deployment slot create fails with Cannot add slots ... plan ... does not support them. Your plan is on the Free or Basic tier, which has no slots. Scale the plan up to Standard or higher: az appservice plan update -g <rg> -n <plan> --sku S1, then create the slot again.

After the swap, production still shows the old version. The swap may not have completed, or you are seeing a cached response. Re-run the swap and wait for it to return, then retry with a cache-busting request such as curl -s "https://<app-name>.azurewebsites.net/?t=$(date +%s)". Confirm the slots' contents with a request to each hostname.

The sticky setting moved (or did not) unexpectedly. A setting is sticky only when you set it with --slot-settings (CLI) or check Deployment slot setting (portal). If it moved during the swap, it was a normal setting - set it again as a slot setting. Confirm with az webapp config appsettings list -g <rg> -n <app> --slot <slot> -o table and look at the slotSetting column.

The staging slot returns 503 or a slow first response right after deploy. The app is still starting. Warm it up before swapping by hitting the staging hostname, or configure WEBSITE_SWAP_WARMUP_PING_PATH. A swap of a cold slot can briefly delay the first production requests, which defeats the purpose.

Swap fails or hangs during warm-up. If you configured WEBSITE_SWAP_WARMUP_PING_PATH, the swap waits for that path to return a healthy status. Confirm the path returns 200 (or one of your WEBSITE_SWAP_WARMUP_PING_STATUSES) on the staging hostname. A path that never returns healthy blocks the swap.

You do not see a staging slot after azd up. Confirm your infra/resources.bicep includes the Microsoft.Web/sites/slots resource and that azd provision (or azd up) completed without errors. List slots with az webapp deployment slot list -g <rg> -n <app> -o table.

Learn more