Azure App Service skill for Codex
Ship through the staging slot, never around it. Codex deploys App Service web apps to a staging slot, checks the health route, and swaps with preview so production settings are applied before traffic moves. Secrets become Key Vault references read by a managed identity, environment-specific values are sticky, and startup command, port and runtime are set explicitly so boot failures are diagnosable.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- azure-app-service Installs to ~/.agents/skills/azure-app-service/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install azure-app-service The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project azure-app-service Installs to .agents/skills/azure-app-service/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/azure-app-service.zipunzip -o azure-app-service.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) azure-app-service PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $azure-app-service, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Deploys and operates web apps on Azure App Service with az CLI and GitHub Actions - Linux plans and runtimes, zip and container deploys, staging deployment slots with sticky settings and swap warm-up, app settings and Key Vault references, health check path, managed identity, Always On, scaling and log streaming. Use when deploying a Node, Python, .NET or Java app or container to App Service, setting up zero-downtime slot swaps, fixing startup failures or 503s, or moving secrets out of app settings.
Use it when
- Deploying code or a container to Azure App Service
- Setting up zero-downtime releases with deployment slots
- An App Service app fails to start or returns 503
- Secrets sit in plain app settings
Use something else when
- The workload is event-driven functions: use azure-functions
- You need to define the plan and app in infrastructure as code: use azure-bicep
How to check the result
The staging slot answers its health route before the swap, production answers after it, and every Key Vault reference shows a resolved status.
The mistake it prevents
Forgetting to mark environment-specific settings as slot settings, so a swap carries staging's database connection into production.
Done well means: code reaches production only through a slot swap after the staging slot answered its health check, secrets are Key Vault references resolved by a managed identity, environment-specific settings are marked sticky, and the app’s startup command, port and runtime are explicit rather than guessed by the platform.
Inspect first
- Read
AGENTS.md, the build scripts, any.github/workflows/*deploy job and any Bicep or Terraform that owns the app. Infrastructure changes go there. - Read the live configuration (needs network and
az login):
az webapp list -g "$RG" -o table
az webapp show -g "$RG" -n "$APP" --query "{plan:appServicePlanId, state:state, host:defaultHostName, httpsOnly:httpsOnly}"
az webapp config show -g "$RG" -n "$APP" \
--query "{stack:linuxFxVersion, alwaysOn:alwaysOn, health:healthCheckPath, startup:appCommandLine, ftps:ftpsState}"
az webapp deployment slot list -g "$RG" -n "$APP" -o table
az webapp list-runtimes --os linux -o table
Treat app setting values as secrets: list names, not values, in chat.
Plan and tier
| Need | Minimum |
|---|---|
| Deployment slots, autoscale | Standard (S1) or Premium v3 (P0v3+) |
| VNet integration, private endpoints | Basic for VNet integration; Premium v3 recommended |
| Production with predictable latency | Premium v3; Always On enabled |
| Dev/test only | Basic or Free (no slots, no Always On on Free) |
Several apps on one plan share its CPU and memory. Put noisy or critical apps on their own plan.
Create
az appservice plan create -g "$RG" -n "$PLAN" --is-linux --sku P0v3
az webapp create -g "$RG" -p "$PLAN" -n "$APP" --runtime "NODE:22-lts" --assign-identity "[system]"
az webapp update -g "$RG" -n "$APP" --https-only true
az webapp config set -g "$RG" -n "$APP" --always-on true --ftps-state Disabled \
--min-tls-version 1.2 --startup-file "node dist/server.js"
az webapp config set -g "$RG" -n "$APP" --generic-configurations '{"healthCheckPath": "/healthz"}'
az webapp deployment slot create -g "$RG" -n "$APP" --slot staging --configuration-source "$APP"
Use a runtime string exactly as printed by az webapp list-runtimes; the format differs
between versions of the CLI and stacks.
Container contract
- The app listens on the port in
PORT(code-based) or the port inWEBSITES_PORT(custom containers, when it is not 80 or 8080). - Bind to
0.0.0.0, notlocalhost. - Startup must finish within the container start time limit (default 230 seconds; raise
with
WEBSITES_CONTAINER_START_TIME_LIMITonly after fixing slow boot). - Pull images with the app’s managed identity: grant it
AcrPullon the registry and setacrUseManagedIdentityCredsto true in the site config.
App settings and secrets
az webapp config appsettings set -g "$RG" -n "$APP" --settings \
NODE_ENV=production \
DATABASE_URL="@Microsoft.KeyVault(VaultName=shop-kv;SecretName=database-url)"
# Settings that must stay with the slot (not travel with the code on swap)
az webapp config appsettings set -g "$RG" -n "$APP" --slot staging \
--slot-settings FEATURE_FLAGS_ENV=staging DATABASE_URL="@Microsoft.KeyVault(VaultName=shop-kv-stg;SecretName=database-url)"
- The app identity needs the
Key Vault Secrets Userrole (RBAC vaults) on the vault. - Check resolution: the portal shows a status per reference; an unresolved reference
arrives in the app as the literal
@Microsoft.KeyVault(...)string. - Omit the secret version so rotation is picked up (App Service re-fetches periodically, and on restart).
- Every settings change restarts the app. Batch changes and do them in staging first.
SCM_DO_BUILD_DURING_DEPLOYMENT=trueasks the platform (Oryx) to run the install and build on deploy. Prefer building in CI and deploying the built artifact.
Deploy to staging, then swap
npm ci && npm run build && zip -r app.zip dist package.json package-lock.json node_modules
az webapp deploy -g "$RG" -n "$APP" --slot staging --src-path app.zip --type zip
curl -fsS "https://$APP-staging.azurewebsites.net/healthz"
# Swap with preview: applies production settings to staging and warms it up first
az webapp deployment slot swap -g "$RG" -n "$APP" --slot staging --target-slot production --action preview
# verify staging again under production settings, then:
az webapp deployment slot swap -g "$RG" -n "$APP" --slot staging --target-slot production --action swap
# abandon instead: --action reset
A swap to production is a production deploy: show the command and wait for user confirmation. Rollback is the same swap run again, since the previous version now sits in staging.
Swap warm-up: App Service pings / on each instance before switching. Point it at a real
readiness route with WEBSITE_SWAP_WARMUP_PING_PATH=/healthz and
WEBSITE_SWAP_WARMUP_PING_STATUSES=200.
GitHub Actions with OIDC (no publish profiles):
permissions: { id-token: write, contents: read }
steps:
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- uses: azure/webapps-deploy@v3
with: { app-name: shop-web, slot-name: staging, package: app.zip }
Federated credential setup for the workflow identity lives in azure-entra-id.
Health check and scaling
- With
healthCheckPathset, instances that fail it repeatedly are removed from the load balancer and eventually replaced. The route must be cheap and not depend on every downstream; a DB outage should not mark all instances dead at once. - Health check acts only when there are 2+ instances. Run production with at least 2.
- Scale out with autoscale rules on CPU or HTTP queue length, or automatic scaling on Premium v3; scale up (bigger SKU) when memory per instance is the limit.
Troubleshooting
az webapp log config -g "$RG" -n "$APP" --docker-container-logging filesystem
az webapp log tail -g "$RG" -n "$APP"
az webapp log download -g "$RG" -n "$APP" --log-file logs.zip
| Symptom | Likely cause |
|---|---|
| “Application Error” / 503 right after deploy | Wrong startup command or port; app listening on localhost |
| Container did not respond to HTTP pings | WEBSITES_PORT mismatch or boot slower than start limit |
| Works in staging, fails after swap | Setting that should be sticky was not, or vice versa |
Secret value looks like @Microsoft.KeyVault(...) | Identity lacks vault role, or vault firewall blocks App Service |
| First request after idle is slow | Always On off, or no warm-up path |
Gotchas
- ARR affinity cookies pin users to an instance; turn off with
--client-affinity-enabled falsefor stateless apps so scale-out actually spreads load. - Local disk under
/homeis shared, slow network storage on Linux; do not use it as a cache or database. - Slot-specific settings changed during swap trigger restarts on the source slot; that is why preview mode exists.
- Deleting a slot or app deletes its content and settings; confirm with the user.
- Basic auth for SCM/FTP should be disabled once CI uses OIDC.
Verification
curlagainst the staging slot returns 200 from/healthzbefore the swap.- After swap, production
/healthzand a key user route return 200; the previous build is in staging for rollback. - Key Vault references show resolved status; no secret values are stored in plain settings.
Report template
**App:** shop-web (Linux, P0v3 x2, NODE:22-lts), httpsOnly, Always On, health /healthz
**Deploy:** zip to staging -> swap with preview -> production (approved by user)
**Settings:** 6 app settings, 2 sticky (FEATURE_FLAGS_ENV, DATABASE_URL), 2 Key Vault references resolved
**Identity:** system-assigned, Key Vault Secrets User on shop-kv
**Rollback:** az webapp deployment slot swap --slot staging --target-slot production What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Azure App Service"
short_description: "Ship through the staging slot, never around it"
default_prompt: "$azure-app-service Set up a staging slot for our Azure web app and a GitHub Actions deploy that swaps after a health check" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.