Skip to content

Custom Domains and Certificates

This recipe shows production custom domain + TLS certificate binding for HTTP-triggered Node.js v4 APIs.

Architecture

flowchart LR
    DNS[Public DNS CNAME or A Record] --> DOMAIN[Custom Domain]
    DOMAIN --> TLS[TLS Certificate Binding]
    TLS --> APP[Function App HTTPS Endpoint]
    APP --> API[HTTP Trigger Handler]

Prerequisites

Use extension bundle v4 in host.json:

{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.*, 5.0.0)"
  }
}

Create a function app for custom domain binding:

az group create --name $RG --location $LOCATION

az storage account create \
  --name $STORAGE_NAME \
  --resource-group $RG \
  --location $LOCATION \
  --sku Standard_LRS

az functionapp create \
  --name $APP_NAME \
  --resource-group $RG \
  --storage-account $STORAGE_NAME \
  --plan $PLAN_NAME \
  --runtime node \
  --runtime-version 20 \
  --functions-version 4

Add a host name and upload/bind certificate:

az functionapp config hostname add \
  --webapp-name $APP_NAME \
  --resource-group $RG \
  --hostname api.contoso.com

az functionapp config ssl upload \
  --name $APP_NAME \
  --resource-group $RG \
  --certificate-file ./certs/api-contoso.pfx \
  --certificate-password <pfx-password>

az functionapp config ssl bind \
  --name $APP_NAME \
  --resource-group $RG \
  --certificate-thumbprint <thumbprint-from-upload> \
  --ssl-type SNI

Set HTTPS-only mode:

az functionapp update \
  --name $APP_NAME \
  --resource-group $RG \
  --set httpsOnly=true

Important hosting plan note: - Flex Consumption does not support App Service managed/platform certificates. - For Flex Consumption, use uploaded certificates or external TLS termination.

Working Node.js v4 Code

const { app } = require("@azure/functions");

app.http("healthz", {
  methods: ["GET"],
  route: "healthz",
  authLevel: "anonymous",
  handler: async (request) => {
    return {
      status: 200,
      headers: {
        "content-type": "application/json",
        "strict-transport-security": "max-age=31536000; includeSubDomains"
      },
      jsonBody: {
        host: request.headers.get("host"),
        status: "ok",
        checkedUtc: new Date().toISOString()
      }
    };
  }
});

Implementation Notes

  • Validate DNS ownership before hostname binding to avoid failed certificate issuance.
  • Keep endpoint-level health checks on custom domains after each certificate rotation.
  • Enforce HTTPS globally (httpsOnly=true) and add HSTS on responses.
  • Track certificate expiration dates and rotate before renewal windows close.

See Also

Sources