Introduction
Azure App Service is the leading PaaS platform for hosting web, mobile, and API apps without managing the underlying infrastructure. In 2026, with the rise of AI and hybrid workloads, mastering advanced App Service means implementing zero-downtime deployments via slots, intelligent autoscaling with KEDA, and native GitHub CI/CD. This advanced tutorial guides a senior developer through building a Node.js Express app, deploying it with Azure CLI and Bicep, setting up slots for blue-green deployments, and scaling based on custom metrics.
Why it matters: Modern apps face unpredictable spikes; App Service shines with seamless integration to Azure Monitor, Application Insights, and Virtual Network. You'll save hours on ops by automating everything. At the end, your app will be production-ready, scalable to 1000+ instances, with proactive monitoring. Ready to go pro? (128 words)
Prerequisites
- Azure account (free or paid with sufficient credit for Premium App Service Plan)
- Azure CLI 2.65+ installed
- Node.js 20+ and npm
- GitHub account with a private repo
- VS Code with Azure App Service and Bicep extensions
- Advanced knowledge of TypeScript, DevOps, and ARM/Bicep
Create the Base Node.js Application
import express, { Request, Response, NextFunction } from 'express';
import helmet from 'helmet';
import cors from 'cors';
const app = express();
const PORT = process.env.PORT || 8080;
app.use(helmet());
app.use(cors());
app.use(express.json());
app.get('/health', (req: Request, res: Response) => {
res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() });
});
app.get('/api/metrics', (req: Request, res: Response) => {
// Simulate CPU-intensive metric for scaling demo
const load = Math.random() * 100;
res.json({ cpuLoad: load, requests: process.env.REQUEST_COUNT || '0' });
});
app.post('/api/increment', (req: Request, res: Response) => {
const count = (parseInt(process.env.REQUEST_COUNT || '0') + 1).toString();
process.env.REQUEST_COUNT = count;
res.json({ incremented: count });
});
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal Server Error' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});This TypeScript Express server includes security (Helmet, CORS), a health check for liveness/readiness probes, and endpoints to simulate CPU load and requests. Env vars like REQUEST_COUNT persist via App Settings. Pitfall: Always type Request/Response to avoid runtime errors; test locally with npx ts-node src/server.ts.
Generate package.json and tsconfig.json
{
"name": "azure-app-service-demo",
"version": "1.0.0",
"main": "dist/server.js",
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"dev": "ts-node src/server.ts"
},
"dependencies": {
"express": "^4.19.2",
"helmet": "^7.1.0",
"cors": "^2.8.5"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.11.19",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}This package.json supports TS-to-JS build for Azure (dist/), with scripts for dev/prod. Install with npm i then npm run build. Pitfall: Don't forget @types for IntelliSense; Azure auto-detects Node via engine in package.json (add "engines": {"node": "20.x"} if needed).
Install Azure CLI and Create Resource Group
#!/bin/bash
# Installer Azure CLI si pas présent
curl -sL https://aka.ms/InstallAzureCLIDeb | bash
# Login (utilisez az login --use-device-code pour headless)
az login
# Variables
RESOURCE_GROUP="rg-appservice-demo-$(date +%Y%m%d)"
LOCATION="westeurope"
APP_NAME="demo-app-$(date +%s)"
# Créer resource group
az group create --name $RESOURCE_GROUP --location $LOCATION
echo "Resource group créé: $RESOURCE_GROUP"
echo "App name généré: $APP_NAME"
export RESOURCE_GROUP=$RESOURCE_GROUP
export APP_NAME=$APP_NAMEThis bash script installs Azure CLI, logs in, and creates a unique RG with timestamp. Run bash setup-azure.sh then source the exports. Pitfall: Use westeurope for low EU latency; check RG quotas with az group list.
Deploy App Service Plan and App via CLI
#!/bin/bash
source setup-azure.sh
exported
# Créer App Service Plan Premium V3 (autoscaling)
az appservice plan create \
--name ${APP_NAME}-plan \
--resource-group $RESOURCE_GROUP \
--sku P1V3 \
--is-linux
# Créer web app Node.js
az webapp create \
--resource-group $RESOURCE_GROUP \
--plan ${APP_NAME}-plan \
--name $APP_NAME \
--runtime "NODE|20-lts" \
--deployment-local-git
# Config app settings pour scaling demo
az webapp config appsettings set \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--settings REQUEST_COUNT=0 WEBSITE_NODE_DEFAULT_VERSION=20
# Build et ZIP deploy
npm run build
zip -r app.zip .
az webapp deployment source config-zip \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--src app.zip
# URL de l'app
echo "App déployée: https://${APP_NAME}.azurewebsites.net/health"This script creates a Premium V3 Linux plan (auto scale-out), Node 20 webapp, sets env vars, builds/zips, and deploys. Test /health. Pitfall: Premium V3 required for slots/custom scaling; ZIP max 2GB, use Git for larger.
Bicep Template for Infrastructure as Code
param location string = resourceGroup().location
param appName string
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: '${appName}-plan'
location: location
sku: {
name: 'P1V3'
tier: 'PremiumV3'
}
kind: 'linux'
}
resource appService 'Microsoft.Web/sites@2023-01-01' = {
name: appName
location: location
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
linuxFxVersion: 'NODE|20-lts'
appSettings: [
{ name: 'REQUEST_COUNT', value: '0' }
{ name: 'WEBSITE_RUN_FROM_PACKAGE', value: '1' }
]
}
}
}
output appUrl string = 'https://${appName}.azurewebsites.net'This Bicep template declares the Plan and App Service with settings. Deploy with az deployment group create --resource-group $RESOURCE_GROUP --template-file main.bicep --parameters appName=$APP_NAME. Pitfall: WEBSITE_RUN_FROM_PACKAGE=1 for run-from-zip without warmup; idempotent and reusable in pipelines.
GitHub Actions Workflow for CI/CD
name: Deploy to Azure App Service
on:
push:
branches: [ main ]
env:
AZURE_WEBAPP_NAME: ${{ secrets.AZURE_WEBAPP_NAME }}
AZURE_WEBAPP_PACKAGE_PATH: 'app.zip'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
- run: zip -r ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} .
- uses: actions/upload-artifact@v4
with:
name: app
path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- download: current
name: Download
uses: actions/download-artifact@v4
with:
name: app
- uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}
package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}This workflow builds/zips on main push, uses OIDC Azure login (via AZURE_CREDENTIALS JSON secret), and deploys ZIP. Add GitHub secrets. Pitfall: ZIP enables run-from-package; test locally with act.
Create Staging Slot and Swap
#!/bin/bash
source setup-azure.sh
exported
# Créer production slot (défaut)
# Créer staging slot
az webapp deployment slot create \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--slot staging
# Déployer sur staging (même ZIP)
zip -r staging.zip .
az webapp deployment source config-zip \
--resource-group $RESOURCE_GROUP \
--name $APP_NAME \
--slot staging \
--src staging.zip
# Test staging
curl https://${APP_NAME}-staging.azurewebsites.net/health
# Swap zero-downtime
az webapp deployment slot swap \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--slot staging \
--target-slot production
echo "Swappé ! Prod: https://${APP_NAME}.azurewebsites.net/health"Slots enable blue-green deployments: deploy/test on staging, atomic swap. Pitfall: Configure traffic % with az webapp traffic ...; auto warmup on Premium.
Best Practices
- Always use slots for zero-downtime and A/B testing.
- Implement KEDA scaling on custom metrics (CPU >80%, requests/sec) via
az monitor autoscale .... - Integrate Application Insights:
az webapp config appsettings set APPINSIGHTS_INSTRUMENTATIONKEY=.... - VNet integration for private endpoints:
az webapp vnet-integration add. - Secrets in Key Vault: Reference via
@Microsoft.KeyVault(...)in Bicep.
Common Errors to Avoid
- Forgetting Premium Plan: Basic/Shared block slots/autoscale; upgrade early.
- ZIP too large: >2GB fails; switch to Git/Containers.
- No health checks: Probes fail, app marked unhealthy; expose
/health. - Hardcoded secrets: Use App Settings or KV, never in code.
Next Steps
- Official docs: Azure App Service
- Advanced Bicep: Bicep Tutorial
- KEDA for event scaling: KEDA on App Service
- Check out our Learni Azure DevOps training for AZ-204 certification.