Posted in:

Back in Feb 2017 I wrote about how you can deploy an Azure Web App by zipping it up and pushing it to App Service with the Kudu REST API. But later that year, a much better new "zip deploy API" was announced, and I wrote another article explaining how to use that. However, more recently, an even newer approach, known as "run from package" has been announced, and is arguably now the best way to deploy your web apps and function apps.

So in this post, I'll show some examples of using "Run from Package" to deploy a simple website. As usual, I'll be using the Azure CLI, from PowerShell.

The way "Run from Package" works is that you simply set up a special App Setting called WEBSITE_RUN_FROM_PACKAGE and its value tells App Service where to find the zip containing your application. There are actually two options available to us. The zip file can be stored at any publicly available URI, so you can just point at a zip file in Azure Blob Storage. Or you can just upload the zip file directly to App Service and update a text file that points at it. We'll see both options in action.

Step 1 - Create an empty web app

We'll start off by creating a resource group, an app service plan and then putting an empty web app in.

$location = "West Europe"
$resGroupName = "RunFromPackageDemo"
az group create -n $resGroupName -l $location

$appServicePlanName = "RunFromPackageDemo"
az appservice plan create -n $appServicePlanName -g $resGroupName --sku B1

$webAppName = "runfrompackagedemo1"
az webapp create -n $webAppName -g $resGroupName --plan $appServicePlanName

It is at this point that we could have used the existing zip deploy API to upload a zip of our application directly to this web app. Behind the scenes, the API would unzip the contents of the uploaded zip into the wwwroot folder. It's very easy to automate this with the Azure CLI:

az webapp deployment source config-zip -n $webAppName `
                -g $resGroupName --src

But let's not do that for now. Instead, we'll see how to use Run from Package.

Step 2 - Upload the zip to blob storage and generate a SAS token

If we are opting for the approach where our WEBSITE_RUN_FROM_PACKAGE points at URI, we need somewhere to store it, and an Azure blob storage container is a good choice. The recommendation is to use a private container, and generate a SAS token to secure access to the zip.

Here's how we could use the Azure CLI to automate creating a new storage account with a private container to store our zip files:

$storageAccountName = "runfrompackagedemo1"
az storage account create -n $storageAccountName -g $resGroupName `
    --sku "Standard_LRS"

# get the connection string and save it as an environment variable
$connectionString = az storage account show-connection-string `
                    -n $storageAccountName -g $resGroupName `
                    --query "connectionString" -o tsv

$containerName = "assets"
az storage container create -n $containerName --public-access off

And let's make a really simple example website to deploy with an index.html called

Write-Output "<h1>Version 1</h1>" > "index.html"
$zipName = ""
Compress-Archive -Path "index.html" -DestinationPath $zipName

And finally let's again use the Azure CLI to upload to blob storage and generate a SAS URL for it. I'm giving mine a five year lifetime in this example. It would appear that the URL needs to be valid for as long as you want the site to work, so you should bear that in mind if you choose this technique. Remember that the SAS will be invalidated if you cycle the keys for your storage account. Normally I consider long-lived SAS tokens to be an anti-pattern, but in this case I'm less concerned since application binaries rarely contain very sensitive information.

# upload the zip to blob storage
$blobName = ""
az storage blob upload -c $containerName -f $zipName -n $blobName

# generate a read-only SAS token that expires in 5 years
$expiry = (Get-Date).ToUniversalTime().AddYears(5).ToString("yyyy-MM-dd\THH\:mm\Z")
$sas = az storage blob generate-sas -c $containerName -n $blobName `
    --permissions r -o tsv `
    --expiry $expiry

# construct a SAS URL out of the blob's URL plus the SAS token
$blobUrl = az storage blob url -c $containerName -n $blobName -o tsv
$sasUrl = "$($blobUrl)?$($sas)"

Step 3 - Point the Web App at the zip in blob storage

Setting the app setting ought to be straightforward. The setting name is WEBSITE_RUN_FROM_PACKAGE and the value is the SAS URL we just generated. But due to a nasty escaping issue using the Azure CLI (see this issue and this issue) we need an ugly fix. (note that this only applies to using the Azure CLI)

# escape the SAS URL to work 
$escapedUrl = $sasUrl.Replace("&","^^^&")

# set the app setting
az webapp config appsettings set -n $webAppName -g $resGroupName `
    --settings "WEBSITE_RUN_FROM_PACKAGE=$escapedUrl"

And that's it. With that one app setting, we've deployed our application. And if we visit our website, we'll see "Version 1".

If we were to repeat the process, creating a file, uploading it to blob storage, generating a SAS URL, and updating the WEBSITE_RUN_FROM_PACKAGE application setting, then we'd very soon see the new version in place.

Why bother?

Now you might be thinking - why go to all this trouble? What was wrong with the previous zip deployment API? And of course, the existing zip API still works just fine and you can keep using it if it meets your needs. But there are some benefits to taking the "Run from Package" approach, which you can read about in more detail here, but I'll briefly summarise them:

  • Ability to rapidly switch back to a previous version without needing to re-upload anything. Your blob storage container functions a bit like a Docker container registry, containing versioned artefacts of your web applications.
  • A much more atomic switchover. Previously your new zip got unzipped over the top of the previous one, meaning that there was a small period during upgrade where your app was taken offline to avoid inconsistency. This approach does still do a site restart, but overall the whole upgrade is much faster.
  • Much faster cold start performance for Azure Functions running on the consumption plan, especially when the zip contains large number of files (e.g. a Node.js application)
  • The wwwroot folder is now read-only. This could be interpreted as a disadvantage as there are some applications that write into their own wwwroot folder - e.g. storing user data in App_Data, but this is no longer considered a good practice for scalable cloud applications, and so being denied this ability is a good thing, and improves predictability - you know exactly what code you're running.

What if I don't want to use blob storage?

Now not everyone will like the idea of needing to point the web app at a blob container, with the inherent possibility that at some point in the future the app could break because someone inadvertently deleted the storage account or cycled the keys.

And "Run from Package" offers a second alternative. With this model, you just set the WEBSITE_RUN_FROM_PACKAGE app setting to the value 1. So let's first use the Azure CLI to update our app setting to use this technique:

az webapp config appsettings set -n $webAppName -g $resGroupName `
    --settings "WEBSITE_RUN_FROM_PACKAGE=1"

Next you need to get your zip file into the D:\home\data\SitePackages folder of your web app and update a packagename.txt file in the same folder to hold the name of the zip file you want to be live. Uploading the zip and editing packagename.txt are both possible with the Kudu REST API, but there's an easier way. When WEBSITE_RUN_FROM_PACKAGE has the value 1, whenever you upload a zip file with the zip deployment API, instead of unzipping it's contents to wwwroot, it will instead save it into SitePackages and update packagename.txt for you.

Suppose we do two deployments of our application using this technique

az webapp deployment source config-zip -n $webAppName `
                    -g $resGroupName --src
az webapp deployment source config-zip -n $webAppName `
                    -g $resGroupName --src

We'll see that is now live, but our SitePackages folder will actually contain both zip files, allowing us to easily switch back if we need to. If we use the Kudu debug console (accessible at to explore what's in SitePackages, here's what we see:

 Volume in drive D is Windows
 Volume Serial Number is E859-323E

 Directory of D:\home\data\SitePackages

01/14/2019  02:49 PM    <DIR>          .
01/14/2019  02:49 PM    <DIR>          ..
01/14/2019  02:47 PM               157
01/14/2019  02:49 PM               157
01/14/2019  02:49 PM                18 packagename.txt
               3 File(s)            332 bytes
               2 Dir(s)  10,737,258,496 bytes free

D:\home\data\SitePackages>type packagename.txt

As you can see, the two uploaded zips have been named with timestamps, and packagename.txt has been updated for us. I like the simplicity of being able to just use the zip deployment API to automate this, but if you wanted to be able to automate rolling back to the previous version, there would be a bit more work involved (see my previous post for some tips on calling the Kudu REST APIs you'd need to use to automate this).


The new "Run from Package" deployment option offers several benefits over previous techniques for deploying Web Apps and Function Apps, and gives you the choice between two places to store your zip files. You can access my full PowerShell & Azure CLI demo script to try this out for yourself in this GitHub Gist. Although I only showed deployment of a very simple static website here, you can use exactly the same technique to deploy any Web App or Function App.

Want to learn more about how easy it is to get up and running with Azure Functions? Be sure to check out my Pluralsight courses Azure Functions Fundamentals and Microsoft Azure Developer: Create Serverless Functions


Comment by Jemma Jem

Very well written, you have great writing skills

Jemma Jem
Comment by Matthew Steeples

Just FYI: There's a typo in the fourth bullet point under "Why Bother". You mention that the folder is write-only when it should be read-only

Matthew Steeples
Comment by Mark Heath

thanks, good spot. I'll get that fixed

Mark Heath
Comment by chris doherty

How does this deployment option work with slot deployments? Is there any benefit to using the run from package method versus the zip deployment method?

chris doherty
Comment by Mark Heath

I haven't tried it with slots, but I presume it ought to work - you'd either have to have a slot specific application setting that points at URL of the zip file, or if you are using the =1 setting, I guess slots would need to have their own D:\home\data\SitePackages - you'd probably need to check that that is the case.

Mark Heath
Comment by Deyan Petrov

When I deploy with:
az functionapp deployment source config-zip -g $resGroup -n $funcAppName --src $projDirPath\
I dont see a setting WEBSITE_RUN_FROM_PACKAGE automatically set to my funciton app.
Does it mean that I am not using the "run from package" option and I should set it manually?
Based on the above I assume that you recommend the usage of "run from package", so I should set WEBSITE_RUN_FROM_PACKAGE=1 when I create the function app, but can I still use then the "az functionapp deployment source config-zip" or should I use something else?

Deyan Petrov
Comment by Mark Heath

Yes, for this technique to work, you should set the WEBSITE_RUN_FROM_PACKAGE yourself. The config-zip technique works differently if that is not used (basically just uploads a zip and then unzips it).
With the setting in place you can just use az functionapp deployment source config-zip, or alternatively, the Azure Functions Core Tools can be used to automate deployments

Mark Heath