Working With Azure AppService and Custom Docroot

4 minute read


Azure App Service is a very flexible hosting platform for websites. It allows them to be developed in many languages (C#/.NET, PHP, Java, Node.js among others) and can be heavily customised by running your own provided Docker container.

It is a service that works very well with automated deployment pipelines from places such as Azure DevOps and Github. But there is one catch, which is the topic of this post. When building these pipelines, you specify an App Service and either the root virtual application (”/” which defaults to “site\wwwroot”) or a sub virtual application (e.g. “/signup” which could point to “site\signup”). The deployment action cannot specify an actual file path (e.g. d:\sites\signup) and is thus, dependent on the virtual application being defined correctly. This isn’t a problem for a virtual application that resides directly in “sites\wwwroot”. Simply deploy to the virtual application and the files go where they’re supposed to go.

Where this breaks down is where the “webroot” of the virtual application is a child of the deployment package. For example, a Composer-managed install of Drupal deploys the content to be served by the web server to the “web” folder as a child of the virtual application root (composer packages that are used by Drupal are in a “vendor” folder that is a sibling of “web” so these scripts are not web-accessible but are easily loaded by the application). This isn’t a problem generally. Simply deploy the files and update the ”/” virtual application in the App Service to “site\wwwroot\web” and it all works. However, if you deploy via Azure DevOps Pipelines and specify ”/” as the virtual application to deploy to in this configuration, you will end up with the following layout:

  • site\wwwroot containing the application with “web” containing the ‘website’
  • site\wwwroot\web containing the application a second time, and thus ending up with site\wwwroot\web\web where the ‘website’ resides.

This is tricky as you cannot specify a physical path in Azure DevOps to “unzip” into, only a specific virtual application, thus causing the above “inception-like” issue.

The Solution (an overview)

The solution is a rather simple one. You need to create another virtual application in your App Service that points to site\wwwroot and configure your pipeline to deploy to that virtual application which will place the files in the “correct” location.

You’ll end up with the following virtual application configurations in the App Service:

  • / => site\wwwroot\web
  • /tempdeploy => site\wwwroot

In your pipeline, you configure the App Service Deployment action to deploy to the /tempdeploy virtual application rather than the / virtual application and all the files will go into their proper locations.

Automating the temporary virtual application

The above solution solves the deployment problem with the Azure DevOps Pipeline but it requires manual intervention to create the temporary virtual application and to remove it after the deployment has completed. Thankfully, this can be automated with a couple of Azure PowerShell tasks within the pipeline.

I have a Github Repo with a variety of scripts I use to help with Azure App Service. Two in particular are Add-TempDeployApp.ps1 and Remove-TempDeployApp.ps1.

These two scripts are used in two Azure PowerShell tasks in the release Pipeline. The Add-TempDeployApp.ps1 is run before the Azure App Service Deployment task to create the temporary virtual application and the Remove-TempDeployApp.ps1 script is run after to clean up the temporary virtual application. To use these scripts, they should be in your application’s root directory (or wherever you want to place them) and bundled up with the build so they’re deployed and available in the release pipeline.

Essentially you will have the following set of tasks:

  1. Azure PowerShell task - Run Add-TempDeployApp.ps1
  2. Azure App Service Deployment - Deploy your application (Remember to configure to deploy to the /tempdeploy virtual application instead of the root or /)
  3. Azure PowerShell task - Run Remove-TempDeployApp.ps1

If you have looked at the scripts, you’ll notice that they are expecting environment variables (variables in the Release Pipeline) to be set. The two mandatory variables are “RESOURCEGROUPNAME” and “WEBSITENAME”. These must be set to the Resource Group and App Service Name respectively. The optional “SLOTNAME” environment variable should only be set if you are deploying your application code to a slot other than the main “production” slot. You’ll also need to update the root virtual application path in both scripts to point to where / is supposed to point to (site\wwwroot\docroot in the scripts on Github).


Deploying code to Azure App Service via Azure DevOps Pipelines is a very powerful feature. Unfortunately, there are a few edge cases where the provided UI/Options don’t “quite” give you enough control. The beauty is in the Azure PowerShell task. This allows a PowerShell script to run within the context of a subscription without having to mess around with authentication tokens or hard-coding credentials. It runs within the same authorization context as the rest of the pipeline (similar to running in the Azure Cloud Shell).

A future blog post will go into a little more detail on the PowerShell scripts and how to discover/configure settings in Azure that the Azure CLI/PowerShell/UI doesn’t provide access to (yet).


This post was guided by this answer from StackOverflow on how to create “virtual applications” in PowerShell.