
Python Appservice Deploy
- 103k installs
- 1.4k repo stars
- Updated August 4, 2026
- microsoft/azure-skills
Python on Azure App Service is a deployment skill for provisioning Python frameworks to Azure App Service Linux.
About
Deploy Python applications (Flask, Django, FastAPI) to Azure App Service Linux. Handles resource group, plan, and web app provisioning with framework detection. Developers use this to quickly move Python services to cloud hosting.
- Auto-detects Flask, Django, FastAPI
- One-command resource creation
- Smart defaults minimize prompts
Python Appservice Deploy by the numbers
- 103,233 all-time installs (skills.sh)
- +15,341 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #23 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
python-appservice-deploy capabilities & compatibility
- Works with
- azure
What python-appservice-deploy says it does
Creates RG + Plan + Web App if missing
npx skills add https://github.com/microsoft/azure-skills --skill python-appservice-deployAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 103k |
|---|---|
| repo stars | ★ 1.4k |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | microsoft/azure-skills ↗ |
How do you deploy Python to Azure App Service?
Deploying Python web applications to managed Azure hosting
Who is it for?
Deploying Python web services to managed cloud hosting
Skip if: Container orchestration, IaC, VNet/Key Vault setup
When should I use this skill?
User asks to deploy Flask, Django, or FastAPI to Azure App Service or says deploy Python to App Service.
What you get
Live Azure App Service Linux web app, resource group, App Service plan, and deployed Python application code
- Deployed App Service web app
- Resource group and plan
By the numbers
- Skill version 1.0.1 from microsoft/azure-skills
- Targets Python 3.14 on Azure App Service Linux P0v3 plan
Files
Python on Azure App Service — Code Deploy
Deploys Python (Flask, Django, FastAPI, generic) code to Azure App Service Linux (P0v3, Python 3.14). Creates RG + Plan + Web App if missing. Hand off to azure-prepare for VNet, Key Vault, databases, or IaC.
MCP tools used: mcp_azure_mcp_subscription_list, mcp_azure_mcp_group_list, mcp_azure_mcp_appservice, mcp_azure_mcp_azd (when azure.yaml is present).
Workflow
1. Resolve context — smart defaults, minimal prompts. Only the app name is interactive; RG (<app>-rg), Plan (<app>-plan), region (current az default or eastus2), subscription are derived. create-app.md §1. 2. Detect framework (advisory, never blocks). detect.md. 3. Choose path — azure.yaml host: appservice → deploy-azd.md; else deploy-azcli.md. 4. Ensure RG → Plan (`P0v3 --is-linux`) → Web App (`--runtime "PYTHON:3.14"`) exist. On transient ARM errors, follow transient-retry.md. create-app.md. 5. Set startup — Flask/Django: none (Oryx auto-detects). FastAPI: always python -m uvicorn main:app --host 0.0.0.0. Other: warn. startup-commands.md. 6. Set `SCM_DO_BUILD_DURING_DEPLOYMENT=true`. 7. Deploy — azd deploy or az webapp deploy --type zip --track-status false. 8. STOP. Print the post-deploy message (post-deploy-message.md) and end the turn.
Hard rules
- ⛔ NO POST-DEPLOY VERIFICATION — after deploy returns, do not run
az webapp log tail,curl,Invoke-WebRequest, or any health probe. App Service needs 2–3 min to warm; a quiet log or early 5xx is not failure. - ⛔ SHELL SAFETY — for
--runtimealways use"PYTHON:3.14"(colon). Never"PYTHON|3.14"(pipe is a shell operator). - ⛔ NEVER `az webapp up` — deprecated. Use Step 7 commands.
- ✅ URL FORMAT — present endpoints as
https://...URLs.
Error Handling
See errors.md for the full symptom → cause → fix matrix. Quick triage: missing plan/app → re-run Step 4; container ping timeout on 8000 → fix startup (Step 5); ModuleNotFoundError after deploy → ensure Step 6 ran, redeploy.
Create RG + App Service Plan + Web App (Linux, P0v3)
Creates resources only when missing — every step is show || create (idempotent against existing user-supplied names).
💡 Shell note: Bash blocks below use\line continuation,||,2>/dev/null,$(…). PowerShell equivalents are shown alongside where the bash form doesn't round-trip — substitute ``for\,2>$nullfor2>/dev/null, and$LASTEXITCODEchecks for||`.
1. Resolve Azure context — minimize prompts
Ask the user at most ONE question (the app name). Derive everything else. If the user's request already names an RG / Plan / region / subscription (e.g. "deploy to my-team-rg in westus3"), use those values verbatim and skip the corresponding derivation — the show || create flow below works against existing resources.
1a. Subscription
az account show --query id -o tsvIf unset, prompt the user to az login. Only call ask_user if multiple subscriptions are configured and no default is set.
1b. App name
Ask the user once:
"What name would you like for your App Service? (Press Enter to auto-generate one.)"
If empty / "any" / "you choose", call the generator script — it implements the slug rules (lowercase, hyphen-collapse, ≤ 40 chars, ^[a-z][a-z0-9-]{1,38}[a-z0-9]$) and an 8-hex-char GUID suffix:
- Bash / zsh: `scripts/generate-app-name.sh` →
APP_NAME=$(./scripts/generate-app-name.sh [folder]) - PowerShell: `scripts/generate-app-name.ps1` →
$appName = & .\scripts\generate-app-name.ps1 [-FolderName <folder>]
Example: folder my-flask-app/ → my-flask-app-a3f9c1d2.
1c. Derived names (use only when user did not specify)
| Resource | Default |
|---|---|
| Resource group | <app-name>-rg |
| App Service Plan | <app-name>-plan |
1d. Region
1. If user specified a region, use it. 2. Else read the CLI default. Suppress the "Configuration is not set" stderr line only when paired with an exit-code check — never blindly drop stderr, since it would also swallow auth/transport errors:
REGION=$(az config get defaults.location -o tsv 2>/dev/null) || REGION="" $region = az config get defaults.location -o tsv 2>$null; if ($LASTEXITCODE -ne 0) { $region = "" }3. Else default to eastus2. 4. Only call ask_user if az group create later fails with a region/quota/availability error.
1e. Show the defaults summary BEFORE creating
Print one concise block so the user can interrupt to override.
Example (illustrative — substitute the actual derived values, do not print verbatim):
Using these defaults for your Python App Service deployment:
• App name : flask-app-demo-27may
• Resource group : flask-app-demo-27may-rg (auto-derived)
• App Service Plan: flask-app-demo-27may-plan (auto-derived)
• Region : eastus2 (CLI default)
• Plan SKU : P0v3 Linux
• Runtime : PYTHON:3.14
Proceeding with create. Reply "stop" within the next message to change any value.Do not call ask_user for confirmation here — just print and proceed.
1f. Transient error handling
On connection-level or 429/5xx errors from any az ... create in §§2–4, see transient-retry.md. Configuration errors (AuthorizationFailed, SkuNotAvailable, QuotaExceeded, etc.) must not be retried — surface them.
2. Resource Group
az group show -n <rg> --only-show-errors 2>/dev/null || \
az group create -n <rg> -l <region>az group show -n <rg> --only-show-errors 2>$null
if ($LASTEXITCODE -ne 0) { az group create -n <rg> -l <region> }3. App Service Plan — Linux, P0v3 by default
⚠️ MANDATORY: Use--is-linuxand--sku P0v3. Do not change OS or SKU unless the user explicitly requests it.
az appservice plan show -n <plan> -g <rg> --only-show-errors 2>/dev/null || \
az appservice plan create -n <plan> -g <rg> --is-linux --sku P0v3 -l <region>az appservice plan show -n <plan> -g <rg> --only-show-errors 2>$null
if ($LASTEXITCODE -ne 0) {
az appservice plan create -n <plan> -g <rg> --is-linux --sku P0v3 -l <region>
}4. Web App — Python 3.14 runtime (Linux)
⚠️ Shell safety: Always use the colon formPYTHON:3.14— never the pipe formPYTHON|3.14. The pipe character is a shell operator in PowerShell, Bash, and cmd, and breaks the command even when quoted in some contexts. The colon form is fully supported byaz webapp create --runtimeand is shell-safe everywhere.
az webapp show -n <app> -g <rg> --only-show-errors 2>/dev/null || \
az webapp create -n <app> -g <rg> -p <plan> --runtime "PYTHON:3.14"az webapp show -n <app> -g <rg> --only-show-errors 2>$null
if ($LASTEXITCODE -ne 0) {
az webapp create -n <app> -g <rg> -p <plan> --runtime "PYTHON:3.14"
}The 8-hex-char GUID suffix from §1b is sufficient for global hostname uniqueness; the optional --domain-name-scope TenantReuse flag (Azure CLI ≥ 2.76, July 2025) is intentionally omitted to stay compatible with older CLIs.
Discover available runtimes
If PYTHON:3.14 is unavailable in the region:
az webapp list-runtimes --os linux --query "[?contains(@, 'PYTHON')]" -o tsvThe output uses the pipe form (e.g., PYTHON|3.14) — convert to colon form before passing to --runtime. Prefer 3.14; fall back to 3.13, then 3.12.
5. Verify
az webapp show -n <app> -g <rg> --query "{name:name, state:state, host:defaultHostName, linuxFx:siteConfig.linuxFxVersion}" -o tableExpected: state: Running, linuxFx: PYTHON|3.14 (Azure stores it in pipe form internally — normal), host: <app>.azurewebsites.net.
Notes
- ⛔ Never use
az webapp up— deprecated. See deploy-azcli.md. - If the user requests a different SKU (e.g.
B1for dev/test), respect it but warn that P0v3 is the documented default. - If a Windows plan is requested, hand off to
azure-prepare.
Deploy via az CLI
Use this path when there is no azure.yaml or it doesn't target appservice.
⛔ NEVER USE `az webapp up` — this command is deprecated. Use the explicit create + deploy commands below.
Prerequisites
az logincomplete- Subscription, resource group, region, and app name decided (see create-app.md)
- App Service Plan (Linux, P0v3) and Web App (Python runtime) exist (see create-app.md)
1. Enable server-side build
az webapp config appsettings set \
-n <app> -g <rg> \
--settings SCM_DO_BUILD_DURING_DEPLOYMENT=trueaz webapp config appsettings set `
-n <app> -g <rg> `
--settings SCM_DO_BUILD_DURING_DEPLOYMENT=trueThis tells Oryx to run pip install -r requirements.txt during deploy.
2. Startup command — skip for Flask/Django, always set for FastAPI
Azure App Service (Oryx) auto-detects Flask and Django — do not set a startup command for these. Skip this step entirely.
For FastAPI, always set the uvicorn startup command, regardless of the Python runtime version. This skill does not rely on Oryx FastAPI auto-detection, so the behavior is identical on every supported runtime (3.12, 3.13, 3.14, …):
az webapp config set -n <app> -g <rg> \
--startup-file "python -m uvicorn main:app --host 0.0.0.0"az webapp config set -n <app> -g <rg> `
--startup-file "python -m uvicorn main:app --host 0.0.0.0"(Replace main:app if the FastAPI entry point differs — e.g., app.main:app.)
For other frameworks (generic WSGI / ASGI / unknown), skip this step and emit the manual-startup warning. See startup-commands.md.
3. Package the code
Zip the project (excluding venv, caches, git, node_modules):
# bash
zip -r app.zip . \
-x ".git/*" -x ".venv/*" -x "venv/*" -x "__pycache__/*" \
-x "*.pyc" -x ".env" -x "node_modules/*"# PowerShell
$exclude = @('.git','.venv','venv','__pycache__','node_modules')
$items = Get-ChildItem -Force | Where-Object { $exclude -notcontains $_.Name }
Compress-Archive -Path $items -DestinationPath app.zip -Force4. Deploy the zip
az webapp deploy \
-n <app> -g <rg> \
--src-path app.zip \
--type zip \
--track-status falseaz webapp deploy `
-n <app> -g <rg> `
--src-path app.zip `
--type zip `
--track-status false💡--track-status falsereturns once the ZIP is accepted by the SCM endpoint — this is not the same as "Oryx build succeeded". The server-sidepip install/ startup-command rendering happens asynchronously after the CLI returns. A zero exit code only confirms the upload + a deployment record. If the site never starts, inspect the build outcome viaaz webapp log deployment list/show— that is the only authoritative confirmation that the build itself succeeded.
5. Stop. Report the endpoint to the user.
After az webapp deploy returns, the skill is done.
ℹ️az webapp deploydoes not initiate a cold start by pinging the site. With--track-status false, it returns as soon as the SCM endpoint accepts the ZIP; the Oryx build and container restart happen asynchronously on the SCM side. The container only warms up when an inbound HTTP request actually hitshttps://<app>.azurewebsites.net— which is why the post-deploy message tells the user to expect a 2–3 minute wait on their first visit.
⛔ Do NOT runaz webapp log tail,curl,Invoke-WebRequest,wget, or any other "verify startup" command. App Service routinely needs 2–3 minutes to warm the container; a quiet log stream or a 5xx in the first couple of minutes is not a failure signal, and running these probes here will mislead the user.
Resolve the host name without hitting the site:
HOST=$(az webapp show -n <app> -g <rg> --query defaultHostName -o tsv)
echo "https://$HOST"$host_ = az webapp show -n <app> -g <rg> --query defaultHostName -o tsv
"https://$host_"Then print the post-deploy message from post-deploy-message.md and end the turn. The user will run az webapp log tail -n <app> -g <rg> themselves if they want to watch logs.
Common pitfalls
| Pitfall | Fix |
|---|---|
| Deployed code missing dependencies | SCM_DO_BUILD_DURING_DEPLOYMENT=true not set — re-run step 1 then redeploy |
| Container ping timeout on port 8000 | Wrong startup command — see startup-commands.md |
| Zip too large (>500 MB) | Exclude .venv, caches; consider .deployment .gitignore-style file |
webapp up examples in older docs | Replace with az webapp create + az webapp deploy (this file) |
Deploy via azd
Use this path when the workspace already has an azure.yaml whose service host is appservice.
When to use
| Condition | Use azd? |
|---|---|
azure.yaml exists AND services.<name>.host: appservice | ✅ Yes |
azure.yaml exists but targets Container Apps / Functions / etc. | ❌ Hand off to azure-prepare |
No azure.yaml | ❌ Use deploy-azcli.md |
Confirm host target
# Look for `host: appservice` under services in azure.yaml
grep -E "host:\s*appservice" azure.yaml# Look for `host: appservice` under services in azure.yaml
Select-String -Path azure.yaml -Pattern 'host:\s*appservice'If no match → use the az CLI path.
Authenticate
azd auth login --check-status || azd auth loginazd auth login --check-status
if ($LASTEXITCODE -ne 0) { azd auth login }Provision (first time only)
If no azd environment exists in this folder:
azd env new <env-name>
azd env set AZURE_LOCATION <region>
# Optional: override default SKU via the template's parameters
azd env set APP_SERVICE_SKU P0v3Then provision + deploy in one call:
azd upDeploy code only (subsequent deploys)
azd deployAfter azd deploy returns, stop. Do not run azd monitor, az webapp log tail, or any HTTP probe.
Report endpoint to the user
⛔ Do NOT probe the endpoint (nocurl, noInvoke-WebRequest) and do NOT tail logs as a verification step. App Service can take 2–3 minutes after deploy before the site responds.
Read the endpoint from azd without hitting the site:
# bash
azd env get-values | grep -E '^SERVICE_.*_URI='# PowerShell
azd env get-values | Select-String "SERVICE_.*_URI"Present the URL with the https:// prefix and the post-deploy message from post-deploy-message.md, then end the turn.
Notes
- ⛔ Do not run
azd init -t <template>in an existing workspace — it can destroy user code. Onlyazd init(no template) is safe inside an existing project. - If
azd upprovisions infra that doesn't match P0v3 Linux, the underlying template owns that decision — don't fight it. Note this to the user. - If
azd deployfails because no environment exists, fall back toazd env newthen re-run.
Framework Detection (Advisory)
Framework detection is advisory only. The deployment never blocks because of an unknown framework.
What to check
1. Confirm Python project — at least one of:
requirements.txtpyproject.toml*.pyfiles in workspace root
2. Scan dependencies in requirements.txt or pyproject.toml:
| Token (case-insensitive) | Detected framework |
|---|---|
flask, Flask | flask |
django, Django | django |
fastapi | fastapi |
gunicorn (alone, no flask) | wsgi-generic |
uvicorn (alone, no fastapi) | asgi-generic |
| None of the above | unknown |
3. Locate the WSGI / ASGI entry point (best-effort):
- Flask common:
app.pyexportingapp,application.py,wsgi.py - Django common:
<project>/wsgi.py - FastAPI common:
main.pyexportingapp
4. Record findings in your working memory so Step 5 (startup) can use them.
Outcomes
| Detection | Step 5 behavior |
|---|---|
flask | Skip startup auto-config. Oryx auto-detects Flask and starts it. |
django | Skip startup auto-config. Oryx auto-detects Django via wsgi.py and starts it. |
fastapi (any Python version) | Always auto-set startup: python -m uvicorn main:app --host 0.0.0.0 (replace main:app with the discovered entry point if different — e.g., app.main:app). The skill does not rely on Oryx FastAPI auto-detection. |
wsgi-generic, asgi-generic, unknown | Skip startup auto-config. Emit warning: "Could not auto-detect a supported framework (only Flask, Django, and FastAPI are auto-configured today). The app will deploy, but you may need to set the startup command manually: `az webapp config set --startup-file '<your-command>'`" |
Priority rule when bothflaskandfastapiappear inrequirements.txt(orpyproject.toml): always treat as FastAPI — set the explicit uvicorn startup command. This is deterministic and avoids relying on import-order or token-order heuristics. Rationale: Flask is happily auto-detected by Oryx with no startup command, but FastAPI requires the explicit uvicorn command to run reliably; if the project actually uses Flask as the served app, the user can override the startup command later, but if it uses FastAPI and we silently picked Flask, the container ping fails.
Important rules
- ⛔ Do not abort deployment when framework is unknown.
- ⛔ Do not try to install Flask, Django, or any framework into the user's project.
- ✅ Always report what was detected so the user has full context.
- ✅ When detection is
wsgi-generic,asgi-generic, orunknown, remember this fact for Step 8 — the post-deploy message must use the unknown-framework template in post-deploy-message.md, which adds an explicit "set a startup command" instruction. The Step 5 warning alone is not enough; the user needs the reminder at the end of the run too.
Example output to surface to the user
Detected: Flask (Python 3.14)
Entry point: app.py
Startup command will be auto-detected by Oryx — no startup command needed.or
Detected: Django (Python 3.14)
Entry point: <project>/wsgi.py
Startup command will be auto-detected by Oryx — no startup command needed.or
Detected: FastAPI (Python 3.14)
Entry point: main.py → app
Setting startup command (always set for FastAPI):
python -m uvicorn main:app --host 0.0.0.0or
Detected: Python project, framework unknown.
The app will deploy, but App Service may not start it correctly until
you set a startup command:
az webapp config set -n <app> -g <rg> --startup-file '<command>'
See startup-commands.md for examples.Troubleshooting
Quick diagnosis flow
Deploy failed?
├─ Check `az webapp log tail` first
├─ Then `az webapp log deployment list` for build-time errors
├─ Then `az webapp config show` to verify runtime + startup
└─ For `Connection reset` / `429` / `502-504` on create commands,
see [transient-retry.md](transient-retry.md)Symptom → Cause → Fix
| Symptom | Likely cause | Fix |
|---|---|---|
Container ... didn't respond to HTTP pings on port: 8000 | Wrong/missing startup command, or app bound to 127.0.0.1 | Set startup to bind 0.0.0.0:8000 (startup-commands.md) |
ModuleNotFoundError: No module named 'flask' (or any dep) | Build skipped, deps not installed | az webapp config appsettings set --settings SCM_DO_BUILD_DURING_DEPLOYMENT=true then redeploy |
gunicorn: command not found | gunicorn missing from requirements.txt | Add gunicorn>=21 to requirements, redeploy |
| Deploy returns 202 but site still shows default page | Async deploy not finished | Wait, or re-run with --track-status true to make the CLI block until completion; check az webapp log deployment list |
OperationNotAllowed: ... requires the plan to be Linux | Plan was created as Windows | Recreate plan with --is-linux (create-app.md) |
Resource group '<rg>' could not be found | RG missing | az group create -n <rg> -l <region> |
An app with name '<app>' already exists | Global name collision | Pick a different name (App Service host names are globally unique) |
az webapp up shown in user's notes | Deprecated command | Replace with az webapp create + az webapp deploy --type zip |
azd up provisions Container Apps instead of App Service | azure.yaml host isn't appservice | Either fix the template's host: or fall back to az CLI path |
| Deployment hangs in "Building..." | Oryx pip install failing on native deps | Run az webapp log deployment list -n <app> -g <rg> to find the deployment ID, then az webapp log deployment show -n <app> -g <rg> --deployment-id <id>; pin known-good versions in requirements |
| 401 / unauthorized on deploy | az login expired or wrong subscription | az login + az account set -s <sub> |
LinuxFxVersion shows `DOCKER\ | ...` | Web app was created as custom container |
Where logs live
⚠️ Prereq for `az webapp log tail` on a fresh app: filesystem logging must be enabled, otherwise the stream stays empty. Run once after the app is created:
>
```bash
az webapp log config -n <app> -g <rg> \
--application-logging filesystem \
--web-server-logging filesystem \
--level information
```
```powershell
az webapp log config -n <app> -g <rg> `
--application-logging filesystem `
--web-server-logging filesystem `
--level information
```
>
az webapp log deployment list/showandaz webapp log downloaddo not require this — they read from a different store.
| Log | Command |
|---|---|
| Live stream | az webapp log tail -n <app> -g <rg> |
| Deployment history | az webapp log deployment list -n <app> -g <rg> |
| Deployment details | az webapp log deployment show -n <app> -g <rg> --deployment-id <id> |
| Full log download | az webapp log download -n <app> -g <rg> |
When to hand off to azure-prepare
Hand off (and stop) if the user needs:
- VNet integration, private endpoints, or Front Door
- Key Vault references for app settings
- A database (Cosmos DB, PostgreSQL, MySQL, SQL)
- Multiple environments with Bicep/Terraform-managed infra
- A non-App-Service target (Container Apps, Functions, AKS)
Tell the user clearly: "This deployment goes beyond a code push — `azure-prepare` will build the full infrastructure plan."
Post-deploy message to the user
After az webapp deploy / azd deploy returns successfully, the skill is done. Do not run any further verification commands.
Hard rules
- ⛔ Never run
az webapp log tailas a "confirm startup" step — logs are often quiet for 1–2 min during build/warm-up; silence is not a failure signal. - ⛔ Never run
curl,Invoke-WebRequest,wget, or any HTTP request against the deployed URL — first request often 502s or times out on a healthy deploy. - ⛔ Never present an early 5xx or quiet log stream as a deploy failure — the deploy succeeded; the platform just isn't warm yet.
- ✅ Always give the user the URL, the wait expectation, and the log command, then stop.
Message template — standard (Flask / Django / FastAPI)
Use this when Step 2 detected a known framework (flask, django, or fastapi). Print exactly this (substituting the real values) as the final output of the skill, then end the turn:
✅ Deployment complete.
🌐 App URL: https://<app>.azurewebsites.net
It can take 2–3 minutes for the site to be reachable while App Service finishes
warming up the container. Open it in your browser after a short wait.
📜 If you want to watch live logs:
az webapp log config -n <app> -g <rg> --application-logging filesystem --level information
az webapp log tail -n <app> -g <rg>
(The first command is a one-time prereq on a fresh app — without it the stream stays empty.)Message template — unknown framework
Use this when Step 2 detected wsgi-generic, asgi-generic, or unknown (i.e. not Flask, Django, or FastAPI). The code is already deployed and SCM_DO_BUILD_DURING_DEPLOYMENT=true is set, but Oryx will not know how to start the app until the user sets a startup command. Print this instead:
✅ Code deployed — but framework not detected.
🌐 App URL: https://<app>.azurewebsites.net
It can take 2–3 minutes for App Service to finish building and start the
container. The site will likely return an error page until you set a
startup command (next step).
⚠️ We could not detect Flask, Django, or FastAPI in your project, so no
startup command was set automatically. Set one with:
az webapp config set -n <app> -g <rg> \
--startup-file "<your-startup-command>"
Examples:
• Generic WSGI (gunicorn):
gunicorn --bind=0.0.0.0 --timeout 600 <module>:<callable>
• Generic ASGI (uvicorn):
python -m uvicorn <module>:<callable> --host 0.0.0.0 --port 8000
See references/startup-commands.md for more guidance.
📜 If you want to watch live logs:
az webapp log config -n <app> -g <rg> --application-logging filesystem --level information
az webapp log tail -n <app> -g <rg>
(The first command is a one-time prereq on a fresh app — without it the stream stays empty.)Replace <module>:<callable> with the user's actual entry point (e.g. app:app, main:application, myapp.wsgi:application). If you can identify a likely entry point from the source code, suggest a concrete command instead of leaving placeholders.
Logging tips (mention only if the user asks for them)
⚠️ Prereq for `az webapp log tail` on a fresh app: filesystem logging must be enabled or the live stream stays empty. Run once:
>
```bash
az webapp log config -n <app> -g <rg> \
--application-logging filesystem --web-server-logging filesystem --level information
```
```powershell
az webapp log config -n <app> -g <rg> `
--application-logging filesystem --web-server-logging filesystem --level information
```
>
Deployment-build logs do not require this — read them with az webapp log deployment list/show.| Log | Command |
|---|---|
| Deployment history | az webapp log deployment list -n <app> -g <rg> |
| Deployment details | az webapp log deployment show -n <app> -g <rg> --deployment-id <id> |
| Full log download | az webapp log download -n <app> -g <rg> |
Picking which template to use
| Detected framework (Step 2) | Template |
|---|---|
flask, django, fastapi | standard |
wsgi-generic, asgi-generic, unknown | unknown framework |
What "success" means here
Either of these is enough to print the success message — do not gate on log output or an HTTP probe:
az webapp deployreturned without an error, orazd deployprintedSUCCESS: Your application was deployed to Azure
Startup Commands by Framework
Linux App Service (Oryx) auto-detects Flask and Django — no startup command is needed for these. FastAPI does NOT rely on Oryx auto-detection in this skill: we always set an explicit uvicorn startup command so the behavior is identical on every supported Python runtime (3.12, 3.13, 3.14, …). This skill also sets a startup command (or emits a manual hint) for non-Flask/Django/FastAPI frameworks.
Flask — no startup command needed
Azure App Service (Oryx) auto-detects Flask and starts it correctly. Do not run `az webapp config set --startup-file` for Flask apps.
Django — no startup command needed
Azure App Service (Oryx) auto-detects Django by finding wsgi.py and starts gunicorn against <project>.wsgi:application automatically. Do not run `az webapp config set --startup-file` for Django apps.
Make sure the project has:
requirements.txtcontainingdjango(and ideallygunicorn; Oryx provides one if missing)<project>/wsgi.pyat a path Oryx can discover (the default Django layout works out of the box)ALLOWED_HOSTSincludes<app>.azurewebsites.net(or*for first-deploy validation — tighten later)
FastAPI — always set the uvicorn startup command
The skill sets the startup command unconditionally for FastAPI, regardless of the Python runtime version. Oryx FastAPI auto-detection is not relied on — an explicit startup command always works and avoids version-dependent surprises:
az webapp config set -n <app> -g <rg> \
--startup-file "python -m uvicorn main:app --host 0.0.0.0"az webapp config set -n <app> -g <rg> `
--startup-file "python -m uvicorn main:app --host 0.0.0.0"Replace main:app with the discovered entry point if different (e.g., app.main:app, src.api:app). The --host 0.0.0.0 flag is mandatory — uvicorn defaults to 127.0.0.1, which causes App Service container-ping timeouts.
Make sure requirements.txt contains both fastapi and uvicorn (or uvicorn[standard]).
Not auto-configured — warn & deploy anyway
When the detected framework is wsgi-generic, asgi-generic, or unknown, deploy the code without setting a startup command and surface this message to the user:
⚠️ This skill auto-configures Flask, Django, and FastAPI only. The app has been deployed, but App Service may not start it until you set a startup command. Choose the matching example below and run it:
Generic WSGI
az webapp config set -n <app> -g <rg> \
--startup-file "gunicorn --bind=0.0.0.0 --timeout 600 <module>:<callable>"az webapp config set -n <app> -g <rg> `
--startup-file "gunicorn --bind=0.0.0.0 --timeout 600 <module>:<callable>"Generic ASGI
az webapp config set -n <app> -g <rg> \
--startup-file "python -m uvicorn <module>:<callable> --host 0.0.0.0 --port 8000"az webapp config set -n <app> -g <rg> `
--startup-file "python -m uvicorn <module>:<callable> --host 0.0.0.0 --port 8000"Django override (only if auto-detection fails)
Oryx auto-detection covers the standard Django layout. Set this manually only if the project uses a non-standard layout and the auto-detected startup doesn't find your WSGI app:
az webapp config set -n <app> -g <rg> \
--startup-file "gunicorn --bind=0.0.0.0 --timeout 600 <project>.wsgi"az webapp config set -n <app> -g <rg> `
--startup-file "gunicorn --bind=0.0.0.0 --timeout 600 <project>.wsgi"Diagnosing a wrong startup command
# Current value
az webapp config show -n <app> -g <rg> --query linuxFxVersion -o tsv
az webapp config show -n <app> -g <rg> --query appCommandLine -o tsv
# Live logs
az webapp log tail -n <app> -g <rg>Look for:
Container <app>_<...> didn't respond to HTTP pings on port: 8000→ app didn't bind to 0.0.0.0 (or 0.0.0.0:8000)ModuleNotFoundError→ wrong<module>:<callable>in startup command, or build didn't install depsgunicorn: command not found/uvicorn: command not found→ addgunicorn/uvicorntorequirements.txt
Why --host 0.0.0.0 matters
App Service forwards traffic to the container on the port the runtime listens on (8000 by default). The startup command must bind to 0.0.0.0 so the platform's ping reaches it — binding to 127.0.0.1 causes ping timeouts.
Transient ARM error handling for az ... create
Load this file only when an az group create, az appservice plan create, or az webapp create command fails. Healthy runs never need it.
Which errors are transient
ARM PUT operations sometimes fail with transient errors:
Connection reset,Connection aborted,ConnectionErrorRead timed out,Max retries exceededBadGatewayConnection,ServiceUnavailableTooManyRequests/ HTTP429- HTTP
502/503/504
These are ARM frontend / network blips, not configuration problems — they almost always succeed on retry.
Do NOT retry on AuthorizationFailed, SubscriptionNotFound, ResourceGroupNotFound, InvalidTemplateDeployment, SkuNotAvailable, QuotaExceeded, or any non-429 4xx. Those are real config errors — surface them to the user.
Rules
- Apply to every
az ... createin create-app.md §§2–4. - Retry silently — do not narrate "let me retry".
- Up to 2 retries (3 attempts total); wait 5s before retry #1 and 15s before retry #2. If the error carries a
Retry-Afterheader, honour that instead. - Wrap the full idempotent pair
(az ... show ...) || (az ... create ...)— never the barecreate. Theshowshort-circuits any partially-succeeded prior attempt and avoids falseConflict/NameAlreadyExists. - After 3 failed attempts, surface the original error with one line of context (e.g.,
"ARM frontend is returning transient errors — please retry in a few minutes").
Use the wrapper scripts
Both shells have a ready-to-call wrapper that implements the loop, the transient-error filter, and the backoff. Prefer these over inlining the loop:
- Bash / zsh (Linux, macOS): `scripts/retry-az-create.sh`
./scripts/retry-az-create.sh \
"az group show -n my-rg --only-show-errors" \
"az group create -n my-rg -l eastus2"- PowerShell (Windows): `scripts/retry-az-create.ps1`
.\scripts\retry-az-create.ps1 `
-ShowCommand "az group show -n my-rg --only-show-errors" `
-CreateCommand "az group create -n my-rg -l eastus2"Both scripts run the show command first, fall through to create on failure, and retry up to 2 times on transient errors only.
<#
.SYNOPSIS
Generates a valid Azure App Service name from a folder name + 8 hex chars.
.DESCRIPTION
Produces a name suitable for `az webapp create -n <name>` that satisfies
Azure App Service naming rules:
- lowercase a-z, 0-9, and hyphens only
- starts with a letter, ends with letter or digit
- total length <= 40 chars (slug truncated as needed)
- regex: ^[a-z][a-z0-9-]{1,38}[a-z0-9]$
.PARAMETER FolderName
Optional. The base name to derive the slug from. Defaults to the current
working directory's leaf name.
.EXAMPLE
.\generate-app-name.ps1
# Uses current folder name, e.g. "my-flask-app" → "my-flask-app-a3f9c1d2"
.EXAMPLE
.\generate-app-name.ps1 -FolderName "my-flask-app"
#>
param(
[string]$FolderName = (Split-Path -Leaf (Get-Location))
)
$ErrorActionPreference = "Stop"
# Lowercase, replace non-[a-z0-9] with '-', collapse repeats, trim hyphens.
$slug = $FolderName.ToLowerInvariant()
$slug = [regex]::Replace($slug, '[^a-z0-9]+', '-')
$slug = [regex]::Replace($slug, '-+', '-')
$slug = $slug.Trim('-')
if ([string]::IsNullOrEmpty($slug)) {
$slug = "app"
}
# 8 hex chars from a fresh GUID (segment before the first '-').
$suffix = [guid]::NewGuid().ToString().Split('-')[0].Substring(0, 8).ToLowerInvariant()
# Reserve 9 chars for "-XXXXXXXX" so the total stays <= 40.
$maxSlugLen = 31
if ($slug.Length -gt $maxSlugLen) {
$slug = $slug.Substring(0, $maxSlugLen).TrimEnd('-')
}
$name = "$slug-$suffix"
# Ensure the name starts with a letter (Azure requirement). Prefix 'a' if not.
if ($name -notmatch '^[a-z]') {
$name = "a$name"
}
# Final length guard.
if ($name.Length -gt 40) {
$name = $name.Substring(0, 40).TrimEnd('-')
}
Write-Output $name
#!/usr/bin/env bash
# generate-app-name.sh
# Generates a valid Azure App Service name from a folder name + 8 hex chars
# of randomness, suitable for `az webapp create -n <name>`.
#
# Rules enforced (matches Azure App Service naming requirements):
# - lowercase a-z, 0-9, and hyphens only
# - starts with a letter, ends with letter or digit
# - total length <= 40 chars (slug truncated as needed)
# - regex: ^[a-z][a-z0-9-]{1,38}[a-z0-9]$
#
# Usage:
# ./generate-app-name.sh [folder-name]
#
# If folder-name is omitted, the current working directory's basename is used.
#
# Examples:
# ./generate-app-name.sh # uses current folder
# ./generate-app-name.sh my-flask-app # → my-flask-app-a3f9c1d2
set -euo pipefail
INPUT="${1:-$(basename "$PWD")}"
# Lowercase, replace non-[a-z0-9] with '-', collapse repeats, trim hyphens.
SLUG=$(echo "$INPUT" \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9]+/-/g; s/-+/-/g; s/^-//; s/-$//')
# Fallback if the slug ended up empty (e.g. folder was only symbols).
if [ -z "$SLUG" ]; then
SLUG="app"
fi
# 8 hex chars from a fresh GUID (segment before the first '-').
if command -v uuidgen >/dev/null 2>&1; then
SUFFIX=$(uuidgen | tr '[:upper:]' '[:lower:]' | cut -d- -f1 | cut -c1-8)
else
# Fallback: /dev/urandom + xxd / od.
SUFFIX=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n' | cut -c1-8)
fi
# Reserve 9 chars for "-XXXXXXXX" so the total stays <= 40.
MAX_SLUG_LEN=31
if [ ${#SLUG} -gt $MAX_SLUG_LEN ]; then
SLUG=$(echo "$SLUG" | cut -c1-$MAX_SLUG_LEN | sed -E 's/-$//')
fi
NAME="${SLUG}-${SUFFIX}"
# Ensure the name starts with a letter (Azure requirement). Prefix 'a' if not.
case "$NAME" in
[a-z]*) ;;
*) NAME="a${NAME}";;
esac
# Final length guard.
NAME=$(echo "$NAME" | cut -c1-40 | sed -E 's/-$//')
echo "$NAME"
<#
.SYNOPSIS
Wraps an idempotent `az ... show || az ... create` pair with silent
retries against transient ARM frontend errors.
.DESCRIPTION
Mirrors retry-az-create.sh. Runs the show command first (short-circuits
a partially-succeeded prior attempt and avoids false NameAlreadyExists
errors). If show fails, runs create. On transient failure (connection
resets, 429/502/503/504, timeouts) retries up to 2 times with 5s then
15s backoff. Non-transient failures surface the original error and
exit 1.
.PARAMETER ShowCommand
The full `az ... show ...` command line as a single string.
.PARAMETER CreateCommand
The full `az ... create ...` command line as a single string.
.EXAMPLE
.\retry-az-create.ps1 `
-ShowCommand "az group show -n my-rg --only-show-errors" `
-CreateCommand "az group create -n my-rg -l eastus2"
.EXAMPLE
.\retry-az-create.ps1 `
-ShowCommand "az appservice plan show -n my-plan -g my-rg --only-show-errors" `
-CreateCommand "az appservice plan create -n my-plan -g my-rg --is-linux --sku P0v3 -l eastus2"
#>
param(
[Parameter(Mandatory)][string]$ShowCommand,
[Parameter(Mandatory)][string]$CreateCommand
)
$ErrorActionPreference = "Continue"
$transient = 'Connection reset|Connection aborted|ConnectionError|Read timed out|BadGatewayConnection|ServiceUnavailable|Max retries exceeded|TooManyRequests|\b429\b|\b50[234]\b'
$backoff = @(5, 15)
for ($attempt = 1; $attempt -le 3; $attempt++) {
# Try `show` silently first; if it fails, try `create`. Capture both
# stdout and stderr into $err so we can inspect for transient patterns.
$err = & {
$script = "$ShowCommand -o none 2>`$null; if (`$LASTEXITCODE -ne 0) { $CreateCommand -o none }"
& powershell -NoProfile -Command $script 2>&1
} | Out-String
if ($LASTEXITCODE -eq 0) {
exit 0
}
if ($err -notmatch $transient) {
Write-Error $err
exit 1
}
if ($attempt -eq 3) {
Write-Error $err
Write-Error "ARM frontend is returning transient errors — please retry in a few minutes."
exit 1
}
Start-Sleep -Seconds $backoff[$attempt - 1]
}
#!/usr/bin/env bash
# retry-az-create.sh
# Wraps the idempotent `(az ... show ...) || (az ... create ...)` pair with
# silent retries against transient ARM frontend errors (connection resets,
# 429/502/503/504, timeouts).
#
# Usage:
# ./retry-az-create.sh "<show-command>" "<create-command>"
#
# Both commands are passed as single shell-quoted strings. The script:
# - runs `show` first (short-circuits any partially-succeeded prior attempt
# and avoids false NameAlreadyExists / Conflict errors);
# - if `show` fails, runs `create`;
# - on transient failure, retries up to 2 times (3 attempts total) with
# 5s then 15s backoff;
# - on non-transient failure, surfaces the original error and exits 1.
#
# Examples:
# ./retry-az-create.sh \
# "az group show -n my-rg --only-show-errors" \
# "az group create -n my-rg -l eastus2"
#
# ./retry-az-create.sh \
# "az appservice plan show -n my-plan -g my-rg --only-show-errors" \
# "az appservice plan create -n my-plan -g my-rg --is-linux --sku P0v3 -l eastus2"
set -uo pipefail
SHOW_CMD="${1:?Usage: $0 \"<show-command>\" \"<create-command>\"}"
CREATE_CMD="${2:?Usage: $0 \"<show-command>\" \"<create-command>\"}"
TRANSIENT='Connection reset|Connection aborted|ConnectionError|Read timed out|BadGatewayConnection|ServiceUnavailable|Max retries exceeded|TooManyRequests|\b429\b|\b50[234]\b'
for attempt in 1 2 3; do
# Run show silently; on failure, run create and capture stderr.
if err=$({ eval "$SHOW_CMD -o none 2>/dev/null" || eval "$CREATE_CMD -o none"; } 2>&1); then
exit 0
fi
if ! echo "$err" | grep -qE "$TRANSIENT"; then
echo "$err" >&2
exit 1
fi
if [ "$attempt" -eq 3 ]; then
echo "$err" >&2
echo "ARM frontend is returning transient errors — please retry in a few minutes." >&2
exit 1
fi
sleep "$([ "$attempt" -eq 1 ] && echo 5 || echo 15)"
done
Related skills
How it compares
Pick python-appservice-deploy for direct Python code-to-App-Service deploys; use azure-prepare when you need networking, secrets, databases, or infrastructure-as-code.
FAQ
What Python frameworks does python-appservice-deploy support?
python-appservice-deploy supports Flask, Django, FastAPI, and generic Python applications on Azure App Service Linux. It targets Python 3.14 on a P0v3 plan and is not intended for Container Apps, Functions, or non-Python runtimes.
Does python-appservice-deploy create Azure resources automatically?
python-appservice-deploy creates a resource group, App Service plan, and web app when they are missing, then deploys application code. For VNet, Key Vault, databases, or IaC, use the azure-prepare skill instead.
Is Python Appservice Deploy safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.