Home / Azure

AzureTroubleshootingIntermediate

App Service deployment fails: 409 Conflict, 401 from Kudu, locked files and Run From Package

A zip deploy or pipeline push to App Service fails or deploys nothing. Read the Kudu deployment log, then fix 409 conflicts, 401/403 on the SCM site, locked files, Run From Package and build issues.

Deepak Bhardwaj7 min read

Applies toAzure App Service (Windows and Linux, code deployments); Kudu/SCM deployment APIs; Azure CLI 2.48.1 or later; Azure DevOps and GitHub Actions deploy tasks

Symptom

The pipeline step that pushes to App Service goes red, or it goes green and the site still serves the old code. Typical messages:

  • Deployment endpoint responded with status code 409
  • 401 Unauthorized or 403 Forbidden from *.scm.azurewebsites.net
  • ERROR_FILE_IN_USE or Web Deploy cannot modify the file ... on the destination because it is locked
  • Failed to deploy web package to App Service with no other detail

Almost every deployment method (zip deploy, az webapp deploy, the DevOps and GitHub Actions tasks) goes through Kudu, the SCM site that sits next to your app. Start by reading what Kudu recorded about the deployment.

Step 1: read the deployment log

APP=app-orders-prod; RG=rg-orders

# Recent deployments and their status
az webapp log deployment list -n $APP -g $RG \
  --query "[].{id:id, status:status, active:active, start:start_time, msg:message}" -o table

# Log lines for the latest deployment
az webapp log deployment show -n $APP -g $RG -o json

Since CLI 2.48.1, these commands (and az webapp deploy) use your Entra sign-in. They work even when basic auth is turned off on the SCM site.

To call the Kudu API directly, get the SCM hostname first. Apps created with unique default hostnames have a regional SCM name, not <app>.scm.azurewebsites.net:

az webapp show -n $APP -g $RG --query "enabledHostNames" -o tsv

SCM=$(az webapp show -n $APP -g $RG --query "enabledHostNames[?contains(@,'.scm.')]|[0]" -o tsv)
TOKEN=$(az account get-access-token --query accessToken -o tsv)
curl -s -H "Authorization: Bearer $TOKEN" "https://$SCM/api/deployments/latest"
curl -s -H "Authorization: Bearer $TOKEN" "https://$SCM/api/deployments/latest/log"

status 4 means success and 3 means failed. Field names and status codes can differ slightly between Kudu builds, so read the message and log_url fields too.

Symptom → cause → fix

Symptom Likely cause Fix
409 Conflict Another deployment is still running, or a previous one is stuck Wait, or restart the SCM site; serialise pipeline runs
409, or zip deploy succeeds but nothing changes WEBSITE_RUN_FROM_PACKAGE set to a URL Update the package at that URL, or switch to 1 and zip deploy
401 from the SCM site Basic auth disabled and the pipeline uses a publish profile Move the pipeline to an Entra service connection; temporary re-enable only as a stopgap
403 from the SCM site SCM access restrictions or private endpoint block the agent Allow the agent's IP on the SCM site, or use a self-hosted agent in the VNet
ERROR_FILE_IN_USE (Windows) The running app holds DLLs open MSDEPLOY_RENAME_LOCKED_FILES=1, take the app offline, or deploy to a slot and swap
Deploy succeeds, app fails to start (Linux) No build ran, so dependencies are missing SCM_DO_BUILD_DURING_DEPLOYMENT=true, or ship a pre-built artifact
Read-only file system at runtime Run From Package mounts wwwroot read-only Write to %TEMP%//tmp or storage, not the app folder

Fixes in detail

409 Conflict

Kudu runs one deployment at a time per app (per slot). Two pipelines that target the same app, or a retry while the first run is still going, return 409.

# Is a deployment still in progress?
az webapp log deployment list -n $APP -g $RG --query "[0].{status:status, start:start_time, end:end_time}" -o table

If a deployment is stuck with no end time, restarting the app also restarts the SCM site:

az webapp restart -n $APP -g $RG

In the pipeline, add an exclusive lock or a concurrency group per environment. For example, in GitHub Actions: concurrency: deploy-prod.

Run From Package

WEBSITE_RUN_FROM_PACKAGE changes how the app is served:

az webapp config appsettings list -n $APP -g $RG \
  --query "[?name=='WEBSITE_RUN_FROM_PACKAGE' || name=='SCM_DO_BUILD_DURING_DEPLOYMENT' || name=='WEBSITE_ENABLE_SYNC_UPDATE_SITE']" -o table
  • 1: zip deploys go to d:\home\data\SitePackages (or /home/data/SitePackages) and are mounted read-only. This is the recommended setting for Windows code apps.
  • A URL: the app runs the package at that blob URL. A zip deploy doesn't replace it. Upload the new package to the blob (or change the URL and SAS), then restart.

Warning

If the setting points to a blob SAS URL, the app stops serving when the SAS expires. Use a managed identity with WEBSITE_RUN_FROM_PACKAGE_BLOB_MI_RESOURCE_ID, or use 1. Check the App Service docs for which plans and OSes support the managed identity option.

Deploy with the modern command and make the type explicit:

az webapp deploy -n $APP -g $RG --src-path ./app.zip --type zip --async false

401 after basic auth was disabled

Many tenants disable SCM basic auth through Azure Policy. Old pipelines that use a publish profile (which holds a username and password) then get 401. Check the setting:

az resource show -g $RG --name scm --namespace Microsoft.Web \
  --resource-type basicPublishingCredentialsPolicies --parent sites/$APP \
  --query properties.allow

The proper fix is to switch the pipeline to an Entra-based connection: an Azure DevOps service connection using workload identity federation, or azure/login with OIDC in GitHub Actions. Then deploy with az webapp deploy or the official task, without a publish profile.

Caution

Re-enabling basic auth (--set properties.allow=true on the same resource) puts shared credentials back on a production deployment endpoint, and policy may revert or block it. Only do it to unblock an urgent release, record an exemption, and turn it off again.

403 from the SCM site

The SCM site has its own access restrictions, and a private endpoint covers it as well as the main site.

az webapp config access-restriction show -n $APP -g $RG \
  --query "{useMain:scmIpSecurityRestrictionsUseMain, scm:scmIpSecurityRestrictions[].{name:name,ip:ipAddress,action:action}}"

If useMain is true, the main site's rules apply to Kudu too, and a hosted build agent's IP is probably blocked. You have two options:

  • Add a scoped allow rule for the SCM site: az webapp config access-restriction add -n $APP -g $RG --scm-site true --rule-name build-agents --action Allow --ip-address <agent-range> --priority 200.
  • For apps behind a private endpoint, run the deployment from a self-hosted agent that can resolve and reach privatelink.azurewebsites.net.

Locked files on Windows

Web Deploy (MSDeploy) can't overwrite DLLs the running process has open. You have three options:

# Let MSDeploy rename locked files instead of failing
az webapp config appsettings set -n $APP -g $RG --settings MSDEPLOY_RENAME_LOCKED_FILES=1
  • Enable the deploy task's "take app offline" option. It drops app_offline.htm so IIS releases the files.
  • Better still, deploy to a staging slot and swap. Files are never locked on the slot that serves traffic.

Linux: deployed, but the app won't start

A zip deploy of source code to Linux runs an Oryx build only when build-on-deploy is enabled:

az webapp config appsettings set -n $APP -g $RG --settings SCM_DO_BUILD_DURING_DEPLOYMENT=true
az webapp log tail -n $APP -g $RG

If the log tail shows ModuleNotFoundError or Cannot find module, either enable the build or ship an artifact with dependencies already included (node_modules, a venv in antenv, a published .NET output). Don't do both: a pre-built zip that also triggers a build wastes time and can replace the dependencies you shipped.

Verify

az webapp log deployment list -n $APP -g $RG --query "[0].{status:status, active:active, msg:message}" -o table
curl -sI https://$APP.azurewebsites.net/healthz

Confirm the deployment is active: true, and check the version endpoint or build number the app exposes. A green pipeline step only proves Kudu accepted the upload.

Prevent it next time

  • Deploy to a staging slot and swap. This avoids locked files and cold starts, and you can roll back by swapping again.
  • Use Entra-based pipeline identities. Keep SCM basic auth off and remove publish profiles from pipeline secrets.
  • Use one concurrency group per app and environment, so two runs never push to the same app at once.
  • Make the pipeline fail when the app's version endpoint doesn't return the new build number after deployment.
  • Keep WEBSITE_RUN_FROM_PACKAGE, SCM_DO_BUILD_DURING_DEPLOYMENT and startup settings in IaC, not in portal edits. Drift in these settings causes most "it deployed but nothing changed" incidents.