---
cuppy_hub: azure
cuppy_path: guides/app-service/deploy-node-app.md
cuppy_section: docs
cuppy_doc: QTDZ-GN4R
cuppy_doc_uuid: 27f05dd7-02fd-4481-803d-56b0b9236049
cuppy_permalink: https://cuppy.io/azure/d/QTDZ-GN4R
cuppy_version: 1
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/azure/docs/guides/app-service/deploy-node-app
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: dd6e66eea6647e4dadba72a55cf90bb96377ef22cc56664a4bee1a5cb86c84af
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Deploy a Node.js TypeScript app to App Service on Linux

`Microsoft.Web` 2025-03-01 is the newest API version with Bicep 0.48.1 types (later ones give `BCP081`). Check the runtime strings live with `az webapp list-runtimes --os-type linux`.

## 1. Infrastructure (`main.bicep`)

```bicep
@description('Web app name: globally unique.')
param appName string

param location string = resourceGroup().location

@description('App Service plan SKU, for example B1, P0v3 or P1v3.')
param skuName string = 'P0v3'

@allowed(['NODE|22-lts', 'NODE|24-lts'])
param linuxFxVersion string = 'NODE|22-lts'

resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = {
  name: 'id-${appName}'
  location: location
}

resource plan 'Microsoft.Web/serverfarms@2025-03-01' = {
  name: 'plan-${appName}'
  location: location
  kind: 'linux'
  sku: {
    name: skuName
  }
  properties: {
    reserved: true
  }
}

resource app 'Microsoft.Web/sites@2025-03-01' = {
  name: appName
  location: location
  kind: 'app,linux'
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${identity.id}': {}
    }
  }
  properties: {
    serverFarmId: plan.id
    httpsOnly: true
    keyVaultReferenceIdentity: identity.id
    siteConfig: {
      linuxFxVersion: linuxFxVersion
      appCommandLine: 'pm2 start dist/server.js --no-daemon'
      healthCheckPath: '/healthz'
      minTlsVersion: '1.2'
      ftpsState: 'Disabled'
      alwaysOn: true
      appSettings: [
        {
          name: 'AZURE_CLIENT_ID'
          value: identity.properties.clientId
        }
        {
          name: 'SCM_DO_BUILD_DURING_DEPLOYMENT'
          value: 'false'
        }
      ]
    }
  }
}

// Turn off basic (user name and password) publishing for SCM and FTP
resource scmBasicAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2025-03-01' = {
  parent: app
  name: 'scm'
  properties: {
    allow: false
  }
}

resource ftpBasicAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2025-03-01' = {
  parent: app
  name: 'ftp'
  properties: {
    allow: false
  }
}

output appUrl string = 'https://${app.properties.defaultHostName}'
output principalId string = identity.properties.principalId
```

Deploy: `az deployment group create -g <rg> -f main.bicep -p appName=<app>`. Grant the identity's `principalId` the data roles it needs (`guides/rbac/assign-roles.md`); `AZURE_CLIENT_ID` makes `DefaultAzureCredential` pick this identity. `alwaysOn` needs Basic or higher.

## 2. The server (`src/server.ts`)

```ts
import { createServer } from "node:http";

// App Service sets PORT; 8080 is only a local default.
const port = Number(process.env.PORT ?? 8080);

const server = createServer((req, res) => {
  if (req.url === "/healthz") {
    // Health check: answer fast, no auth, no calls to dependencies.
    res.writeHead(200, { "content-type": "text/plain" }).end("ok");
    return;
  }
  res.writeHead(200, { "content-type": "application/json" });
  res.end(JSON.stringify({ message: "hello", instance: process.env.WEBSITE_INSTANCE_ID ?? "local" }));
});

server.listen(port, () => console.log(`listening on ${port}`));

// Close cleanly when the container is stopped.
process.on("SIGTERM", () => server.close(() => process.exit(0)));
```

## 3. Build and deploy

```bash
npm ci && npm run build          # tsc → dist/server.js
npm prune --omit=dev             # ship production dependencies only
zip -r app.zip dist node_modules package.json
az webapp deploy -g <rg> -n <app> --src-path app.zip --type zip
```

Build automation is off (`SCM_DO_BUILD_DURING_DEPLOYMENT=false`), so the zip is run as-is: it must contain `dist` and `node_modules`. To build on the platform instead, set it to `true` and zip the sources; see `reference/app-service/configuration-and-networking.md` for what that build runs. `az webapp deploy` authenticates with your Entra sign-in, so it works with basic publishing credentials disabled; publish profiles and FTP don't.

## 4. Check it

```bash
az webapp log tail -g <rg> -n <app>
curl -sS https://<app>.azurewebsites.net/healthz
```

If the container never becomes ready, the cause is almost always the port, the startup command or a missing file in the zip: `reference/app-service/errors.md`. For CI, use GitHub Actions with OIDC: `patterns/github-actions-to-azure.md`. For traces and metrics: `guides/monitor/instrument-node-app-with-opentelemetry.md`.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 107 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/azure/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
