This guide documents a working, repeatable approach to deploying an Express + Nunjucks application (including monorepo/subfolder apps) to Azure App Service (Linux, code-based) using GitHub Actions and a Publish Profile.
This approach avoids Docker, ACR, private DNS, and container pull issues.
- No Docker images
- No ACR / registry DNS
- No VNet / private endpoint dependency
- Uses standard Azure App Service runtime
- Simple GitHub Actions workflow
- Ideal for secured environments (HMCTS-style platforms)
- Azure CLI installed and logged in:
az login
- GitHub repo with your Express app
- Your app must listen on process.env.PORT:
const port = process.env.PORT || 3000; app.listen(port);
- .github/workflows must live at repo root
- Deploy subfolder, not repo root, in monorepos
- Publish profile secret must be a repository secret
- Express must listen on process.env.PORT
- Static assets must be explicitly served
# Azure
RG="[Your resource group]"
LOCATION="uksouth"
# App Service
PLAN_NAME="[Your plan name]"
WEBAPP_NAME="[Your app name]" # must be globally unique
# Runtime
RUNTIME="NODE:20-lts"
# Monorepo path (folder containing package.json)
APP_DIR="[path to package.json]"
# (Optional) Set subscription:
az account set --subscription <SUBSCRIPTION_ID> az appservice plan create \
--name "$PLAN_NAME" \
--resource-group "$RG" \
--location "$LOCATION" \
--is-linux \
--sku B1 az webapp create \
--resource-group "$RG" \
--plan "$PLAN_NAME" \
--name "$WEBAPP_NAME" \
--runtime "$RUNTIME"
#Verify it is not container-based
az webapp show \
--name "$WEBAPP_NAME" \
--resource-group "$RG" \
--query "{kind:kind, linuxFxVersion:siteConfig.linuxFxVersion}" \
-o json- kind includes app,linux
- linuxFxVersion is not DOCKER|...
az webapp config appsettings set \
--name "$WEBAPP_NAME" \
--resource-group "$RG" \
--settings NODE_ENV=productionSet the basic auth to true before generating the publish profile
az resource update --resource-group "$RG" \
--name scm \
--namespace Microsoft.Web \
--resource-type basicPublishingCredentialsPolicies \
--parent sites/"$WEBAPP_NAME"\
--set properties.allow=true az webapp deployment list-publishing-profiles \
--name "$WEBAPP_NAME" \
--resource-group "$RG" \
--xml > publishProfile.xmlecho "publishProfile.xml" >> .gitignore
git add .gitignore
git commit -m "Ignore Azure publish profile"pbcopy < publishProfile.xml- Repo → Settings
- Secrets and variables → Actions
- New repository secret
- Name: AZURE_WEBAPP_PUBLISH_PROFILE
- Value: paste the full XML
Create this file at repo root:
.github/workflows/deploy.ymlIn this new file copy and paste:
name: Deploy to Azure Web App
on:
push:
branches: ["main"]
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: packages/hmcts-docs/package-lock.json
- name: Install dependencies
working-directory: packages/hmcts-docs
run: npm ci
- name: Build (if present)
working-directory: packages/hmcts-docs
run: npm run build --if-present
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v3
with:
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: packages/hmcts-docs
Browser console shows:
Refused to apply style ... MIME type ('text/html')
This means the CSS URL is returning HTML, not CSS.
Open:
https://<WEBAPP_NAME>.scm.azurewebsites.net/DebugConsoleRun:
find /home/site/wwwroot -maxdepth 6 -type f -name "*.css"Example output:
./packages/hmcts-frontend/dist/hmcts.cssIf your HTML references:
/assets/hmcts.css
Add this before routes in your Express app:
const path = require("path");
app.use(
"/assets",
express.static(
path.join(__dirname, "packages", "hmcts-frontend", "dist")
)
);After redeploy: [Content-Type: text/css] (https://<WEBAPP_NAME>.azurewebsites.net/assets/hmcts.css) should be returned
az webapp log config \
--name "$WEBAPP_NAME" \
--resource-group "$RG" \
--application-logging filesystem
az webapp log tail \
--name "$WEBAPP_NAME" \
--resource-group "$RG"az webapp restart \
--name "$WEBAPP_NAME" \
--resource-group "$RG"az webapp delete \
--name "$WEBAPP_NAME" \
--resource-group "$RG"
az appservice plan delete \
--name "$PLAN_NAME" \
--resource-group "$RG" \
--yes