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 409401 Unauthorizedor403 Forbiddenfrom*.scm.azurewebsites.netERROR_FILE_IN_USEorWeb Deploy cannot modify the file ... on the destination because it is lockedFailed to deploy web package to App Servicewith 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 tod:\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 use1. 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=trueon 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.htmso 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_DEPLOYMENTand startup settings in IaC, not in portal edits. Drift in these settings causes most "it deployed but nothing changed" incidents.