Installation
Install with CLI
Recommended
gh skills-hub install python-appservice-deploy Don't have the extension? Run gh extension install samueltauil/skills-hub first.
Download and extract to your repository:
.github/skills/python-appservice-deploy/ Extract the ZIP to .github/skills/ in your repo. The folder name must match python-appservice-deploy for Copilot to auto-discover it.
Skill Files (13)
SKILL.md 2.7 KB
---
name: python-appservice-deploy
description: "Deploy Python (Flask/Django/FastAPI) code to Azure App Service Linux. WHEN: \"Flask App Service\", \"Django App Service\", \"FastAPI App Service\", \"deploy Python to App Service\". DO NOT USE FOR: Container Apps, Functions, non-Python, Terraform/Bicep/IaC, full infra โ use azure-prepare."
license: MIT
metadata:
author: Microsoft
version: "1.0.1"
---
# 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](references/create-app.md) ยง1.
2. **Detect framework** (advisory, never blocks). [detect.md](references/detect.md).
3. **Choose path** โ `azure.yaml` host: appservice โ [deploy-azd.md](references/deploy-azd.md); else [deploy-azcli.md](references/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](references/transient-retry.md). [create-app.md](references/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](references/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](references/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 `--runtime` always 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](references/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.
references/
create-app.md 6.0 KB
# 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>$null` for `2>/dev/null`, and `$LASTEXITCODE` checks 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
```bash
az account show --query id -o tsv
```
If 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`](../scripts/generate-app-name.sh) โ `APP_NAME=$(./scripts/generate-app-name.sh [folder])`
- PowerShell: [`scripts/generate-app-name.ps1`](../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:
```bash
REGION=$(az config get defaults.location -o tsv 2>/dev/null) || REGION=""
```
```powershell
$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](transient-retry.md). Configuration errors (`AuthorizationFailed`, `SkuNotAvailable`, `QuotaExceeded`, etc.) must **not** be retried โ surface them.
## 2. Resource Group
```bash
az group show -n <rg> --only-show-errors 2>/dev/null || \
az group create -n <rg> -l <region>
```
```powershell
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-linux` and `--sku P0v3`. Do not change OS or SKU unless the user explicitly requests it.
```bash
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>
```
```powershell
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** form `PYTHON:3.14` โ never the pipe form `PYTHON|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 by `az webapp create --runtime` and is shell-safe everywhere.
```bash
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"
```
```powershell
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:
```bash
az webapp list-runtimes --os linux --query "[?contains(@, 'PYTHON')]" -o tsv
```
The 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
```bash
az webapp show -n <app> -g <rg> --query "{name:name, state:state, host:defaultHostName, linuxFx:siteConfig.linuxFxVersion}" -o table
```
Expected: `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](deploy-azcli.md).
- If the user requests a different SKU (e.g. `B1` for dev/test), respect it but warn that **P0v3** is the documented default.
- If a Windows plan is requested, hand off to `azure-prepare`.
deploy-azcli.md 4.7 KB
# 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 login` complete
- Subscription, resource group, region, and app name decided (see [create-app.md](create-app.md))
- App Service Plan (Linux, P0v3) and Web App (Python runtime) exist (see [create-app.md](create-app.md))
## 1. Enable server-side build
```bash
az webapp config appsettings set \
-n <app> -g <rg> \
--settings SCM_DO_BUILD_DURING_DEPLOYMENT=true
```
```powershell
az webapp config appsettings set `
-n <app> -g <rg> `
--settings SCM_DO_BUILD_DURING_DEPLOYMENT=true
```
This 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, โฆ):
```bash
az webapp config set -n <app> -g <rg> \
--startup-file "python -m uvicorn main:app --host 0.0.0.0"
```
```powershell
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](startup-commands.md).
## 3. Package the code
Zip the project (excluding venv, caches, git, node_modules):
```bash
# bash
zip -r app.zip . \
-x ".git/*" -x ".venv/*" -x "venv/*" -x "__pycache__/*" \
-x "*.pyc" -x ".env" -x "node_modules/*"
```
```powershell
# PowerShell
$exclude = @('.git','.venv','venv','__pycache__','node_modules')
$items = Get-ChildItem -Force | Where-Object { $exclude -notcontains $_.Name }
Compress-Archive -Path $items -DestinationPath app.zip -Force
```
## 4. Deploy the zip
```bash
az webapp deploy \
-n <app> -g <rg> \
--src-path app.zip \
--type zip \
--track-status false
```
```powershell
az webapp deploy `
-n <app> -g <rg> `
--src-path app.zip `
--type zip `
--track-status false
```
> ๐ก `--track-status false` returns once the ZIP is **accepted by the SCM endpoint** โ this is **not** the same as "Oryx build succeeded". The server-side `pip 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 via `az 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 deploy` does **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 hits `https://<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 run** `az 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:
```bash
HOST=$(az webapp show -n <app> -g <rg> --query defaultHostName -o tsv)
echo "https://$HOST"
```
```powershell
$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](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](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-azd.md 2.3 KB
# 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](deploy-azcli.md) |
## Confirm host target
```bash
# Look for `host: appservice` under services in azure.yaml
grep -E "host:\s*appservice" azure.yaml
```
```powershell
# 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
```bash
azd auth login --check-status || azd auth login
```
```powershell
azd auth login --check-status
if ($LASTEXITCODE -ne 0) { azd auth login }
```
## Provision (first time only)
If no `azd` environment exists in this folder:
```bash
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 P0v3
```
Then provision + deploy in one call:
```bash
azd up
```
## Deploy code only (subsequent deploys)
```bash
azd deploy
```
After `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** (no `curl`, no `Invoke-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
# bash
azd env get-values | grep -E '^SERVICE_.*_URI='
```
```powershell
# 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](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. Only `azd init` (no template) is safe inside an existing project.
- If `azd up` provisions infra that doesn't match P0v3 Linux, the underlying template owns that decision โ don't fight it. Note this to the user.
- If `azd deploy` fails because no environment exists, fall back to `azd env new` then re-run.
detect.md 3.6 KB
# 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.txt`
- `pyproject.toml`
- `*.py` files 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.py` exporting `app`, `application.py`, `wsgi.py`
- Django common: `<project>/wsgi.py`
- FastAPI common: `main.py` exporting `app`
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 both `flask` and `fastapi` appear in `requirements.txt` (or `pyproject.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`, or `unknown`, **remember this fact for Step 8** โ the post-deploy message must use the **unknown-framework template** in [post-deploy-message.md](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.0
```
or
```
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.
```
errors.md 3.7 KB
# 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](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](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 | Recreate with `--runtime "PYTHON:3.14"` (colon form is shell-safe) |
## 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/show` and `az webapp log download` do **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.md 4.3 KB
# 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 tail` as 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 deploy` returned without an error, **or**
- `azd deploy` printed `SUCCESS: Your application was deployed to Azure`
startup-commands.md 4.3 KB
# 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.txt` containing `django` (and ideally `gunicorn`; Oryx provides one if missing)
- `<project>/wsgi.py` at a path Oryx can discover (the default Django layout works out of the box)
- `ALLOWED_HOSTS` includes `<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:
```bash
az webapp config set -n <app> -g <rg> \
--startup-file "python -m uvicorn main:app --host 0.0.0.0"
```
```powershell
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
```bash
az webapp config set -n <app> -g <rg> \
--startup-file "gunicorn --bind=0.0.0.0 --timeout 600 <module>:<callable>"
```
```powershell
az webapp config set -n <app> -g <rg> `
--startup-file "gunicorn --bind=0.0.0.0 --timeout 600 <module>:<callable>"
```
### Generic ASGI
```bash
az webapp config set -n <app> -g <rg> \
--startup-file "python -m uvicorn <module>:<callable> --host 0.0.0.0 --port 8000"
```
```powershell
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:
```bash
az webapp config set -n <app> -g <rg> \
--startup-file "gunicorn --bind=0.0.0.0 --timeout 600 <project>.wsgi"
```
```powershell
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
```bash
# 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 deps
- `gunicorn: command not found` / `uvicorn: command not found` โ add `gunicorn` / `uvicorn` to `requirements.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-retry.md 2.3 KB
# 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`, `ConnectionError`
- `Read timed out`, `Max retries exceeded`
- `BadGatewayConnection`, `ServiceUnavailable`
- `TooManyRequests` / HTTP `429`
- 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 ... create` in [create-app.md](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-After` header, honour that instead.
- **Wrap the full idempotent pair** `(az ... show ...) || (az ... create ...)` โ never the bare `create`. The `show` short-circuits any partially-succeeded prior attempt and avoids false `Conflict` / `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)
```bash
./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)
```powershell
.\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.
scripts/
generate-app-name.ps1 1.7 KB
<#
.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
generate-app-name.sh 1.8 KB
#!/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"
retry-az-create.ps1 2.1 KB
<#
.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]
}
retry-az-create.sh 1.9 KB
#!/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
License (MIT)
View full license text
MIT License Copyright 2025 (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.