Package & Deploy to Azure Gov
What you'll learn
~30 min- Configure Next.js standalone output mode for Azure App Service deployment
- Execute the critical static file copy step that standalone mode requires
- Deploy to Azure Government App Service using zip deployment
Why deployment is different on Azure Gov
Vercel is not an option. Netlify is not an option. The DS platform runs on Azure Government App Service, and the deployment process is specific: build a standalone Next.js bundle, copy the static assets that standalone mode intentionally excludes, configure environment variables for gov cloud endpoints, set up health checks, and zip-deploy to the App Service instance.
Every team that deploys Next.js to Azure App Service hits the same problem the first time: the application starts, the API routes work, but every page loads without CSS, images, or client-side JavaScript. The static files are missing. This lesson makes sure that does not happen to you.
Standalone output mode
Next.js standalone mode produces a self-contained server that includes only the files needed to run in production — no node_modules directory (it bundles dependencies), no dev tooling, no test files. The output is a minimal folder that can be zipped and deployed.
next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = { output: 'standalone', // Required for Azure App Service — it expects the app on 8080 serverExternalPackages: ['applicationinsights'],};
export default nextConfig;The applicationinsights SDK uses native modules that cannot be bundled by Next.js’s output tracing. Adding it to serverExternalPackages tells Next.js to leave it as a standard require() instead of trying to bundle it. Without this, the build succeeds but the server crashes on startup with a missing native module error.
What standalone mode produces
After npm run build, the output looks like this:
.next/ standalone/ server.js # The production server entry point package.json node_modules/ # Bundled dependencies (much smaller than full) .next/ server/ # Server-side rendered pages and API routes BUILD_ID # NOTE: .next/static/ is NOT here static/ # Client-side JS, CSS, images chunks/ css/ media/public/ # Your public/ assets (favicons, manifests, etc.)Notice the gap: .next/standalone/.next/static/ does not exist. The static assets are in .next/static/, one level up from the standalone folder. The public/ directory is also not inside standalone.
This is intentional. The Next.js team assumes you will serve static files from a CDN in production. Azure App Service does not have a built-in CDN separation — it serves everything from the same process. So you copy the files manually.
The critical copy step
This is the step that every team misses the first time. It is two commands:
cp -r .next/static .next/standalone/.next/staticcp -r public .next/standalone/publicWithout these, your deployed application returns HTML with references to /_next/static/chunks/main-abc123.js and /_next/static/css/styles-def456.css, but those files do not exist on the server. The browser shows a blank page or unstyled HTML with no interactivity.
If you remember one thing from this lesson, remember these two cp commands. They are not in every Next.js tutorial. They are not in the Azure App Service quickstart. They are the difference between a working deployment and a broken one that shows no errors in the server logs because the server is fine — it is the client that cannot find its assets.
The deployment script
The prompt
Generate a deployment script called deploy.sh for our Next.js 15.5 platformtargeting Azure Government App Service.
Requirements:1. Build step: npm ci && npm run build2. Static file copy: - cp -r .next/static .next/standalone/.next/static - cp -r public .next/standalone/public3. Create a zip of the .next/standalone directory4. Deploy using az webapp deploy: - Resource group and app name from environment variables - Use --type zip - Azure Gov cloud (az cloud set --name AzureUSGovernment)5. Health check: after deploy, curl the /api/health endpoint and verify 200 response within 60 seconds (retry every 5 seconds)6. Environment variables to set on the App Service: - NODE_ENV=production - PORT=8080 (Azure App Service requirement) - WEBSITES_PORT=8080 - KEY_VAULT_NAME (from env) - AZURE_AUTHORITY_HOST=https://login.microsoftonline.us - APPLICATIONINSIGHTS_CONNECTION_STRING (from Key Vault, not hardcoded)7. Include a --dry-run flag that builds and packages but does not deploy8. Exit with error code if any step fails (set -euo pipefail)What the script produces
The AI generates a script with these key sections:
#!/usr/bin/env bashset -euo pipefail
# Configuration from environmentRESOURCE_GROUP="${AZURE_RESOURCE_GROUP:?Set AZURE_RESOURCE_GROUP}"APP_NAME="${AZURE_APP_NAME:?Set AZURE_APP_NAME}"DRY_RUN="${1:-}"
echo "=== Building Next.js standalone ==="npm ci --ignore-scripts=falsenpm run build
echo "=== Copying static assets ==="cp -r .next/static .next/standalone/.next/staticcp -r public .next/standalone/public
echo "=== Creating deployment package ==="cd .next/standalonezip -r ../../deploy.zip . -x "*.map"cd ../..
PACKAGE_SIZE=$(du -sh deploy.zip | cut -f1)echo "Package size: ${PACKAGE_SIZE}"
if [ "$DRY_RUN" = "--dry-run" ]; then echo "=== Dry run — skipping deployment ===" echo "Package ready: deploy.zip" exit 0fi
echo "=== Deploying to Azure Gov ==="az cloud set --name AzureUSGovernmentaz webapp deploy \ --resource-group "$RESOURCE_GROUP" \ --name "$APP_NAME" \ --type zip \ --src-path deploy.zip
echo "=== Health check ==="HEALTH_URL="https://${APP_NAME}.azurewebsites.us/api/health"for i in {1..12}; do STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL" || echo "000") if [ "$STATUS" = "200" ]; then echo "Health check passed (attempt ${i})" exit 0 fi echo "Waiting for health check... (attempt ${i}/12, status: ${STATUS})" sleep 5done
echo "ERROR: Health check failed after 60 seconds"exit 1Azure Government App Service uses the .azurewebsites.us domain, not .azurewebsites.net. The health check URL, CORS origins, and any external integrations that reference your app URL must use the .us domain.
Environment variables for Azure Gov
Azure App Service environment variables are configured through the Azure portal or the az CLI. These are the variables the DS platform requires in production:
| Variable | Value | Notes |
|---|---|---|
NODE_ENV | production | Enables Next.js production optimizations |
PORT | 8080 | Azure App Service expects the app on this port |
WEBSITES_PORT | 8080 | Tells the App Service reverse proxy which port to forward to |
KEY_VAULT_NAME | ds-platform-kv | Your Key Vault instance name |
AZURE_AUTHORITY_HOST | https://login.microsoftonline.us | Gov AD endpoint |
NEXTAUTH_URL | https://ds-platform.azurewebsites.us | Full app URL for next-auth callbacks |
NEXTAUTH_SECRET | (from Key Vault) | JWT signing key — never in App Service config directly |
az webapp config appsettings set \ --resource-group "$RESOURCE_GROUP" \ --name "$APP_NAME" \ --settings \ NODE_ENV=production \ PORT=8080 \ WEBSITES_PORT=8080 \ KEY_VAULT_NAME=ds-platform-kv \ AZURE_AUTHORITY_HOST=https://login.microsoftonline.us \ NEXTAUTH_URL=https://ds-platform.azurewebsites.usThe NEXTAUTH_SECRET is the JWT signing key. If it is in App Service configuration, it is visible to anyone with access to the Azure portal. Fetch it from Key Vault at runtime instead. Your auth.ts should call getSecret('nextauth-secret') during initialization.
Health check endpoint
Azure App Service pings a health check endpoint to determine if your app is ready to receive traffic. If the health check fails, App Service restarts the container.
import { NextResponse } from 'next/server';
export async function GET() { // Basic health: server is running and can respond const health = { status: 'healthy', timestamp: new Date().toISOString(), version: process.env.BUILD_ID ?? 'unknown', environment: process.env.NODE_ENV, };
// Optional: verify downstream dependencies // Only if you want health to reflect DB connectivity // Caution: this makes health checks slower and can // cause restart loops if the DB is temporarily down
return NextResponse.json(health, { status: 200 });}Do not add database connectivity checks to the health endpoint unless you want App Service to restart your app every time Azure SQL has a blip. A healthy app that cannot reach the database should return 200 (it is alive) and handle the DB failure gracefully at the route level. Reserve dependency checks for a separate /api/ready endpoint.
Configure the health check in the Azure portal: App Service > Health check > Path: /api/health. Set the threshold to 3 failed checks before restart.
See it in action
Common deployment failures
| Symptom | Cause | Fix |
|---|---|---|
| Blank page, no CSS or JS | Static files not copied into standalone | Run cp -r .next/static .next/standalone/.next/static and cp -r public .next/standalone/public before zipping |
EADDRINUSE: address already in use :::8080 | Previous process did not shut down | App Service handles this — it restarts the container. If persistent, check for a custom server.js that hardcodes a port |
MODULE_NOT_FOUND: applicationinsights | Native module not in standalone bundle | Add applicationinsights to serverExternalPackages in next.config.ts |
| Health check fails, app restarts in loop | Health endpoint checks DB, DB is slow to connect | Simplify health check to return 200 without dependency checks |
az webapp deploy returns 403 | PAT or service principal lacks deployment permissions | Verify the identity has the Website Contributor role on the App Service resource |
| CORS errors in browser | NEXTAUTH_URL uses .azurewebsites.net instead of .us | Update to .azurewebsites.us for Azure Government |
Deployment to multiple environments
The DS platform deploys to three environments: dev, staging, and production. The deployment script is the same; only the environment variables change.
# DevAZURE_RESOURCE_GROUP=DS-Platform-Dev-RG AZURE_APP_NAME=ds-platform-dev ./deploy.sh
# StagingAZURE_RESOURCE_GROUP=DS-Platform-Staging-RG AZURE_APP_NAME=ds-platform-staging ./deploy.sh
# Production (requires manual approval in Azure Pipelines)AZURE_RESOURCE_GROUP=DS-Platform-Prod-RG AZURE_APP_NAME=ds-platform-prod ./deploy.shThis deploy.sh script is useful for understanding the process and for emergency deployments. In the next lesson, you will automate this entire workflow in Azure Pipelines — including the approval gate before production. Once the pipeline is working, manual deployments should only happen when the pipeline itself is broken.
You run npm run build with output: 'standalone' in next.config.ts, then immediately zip the .next/standalone directory and deploy it to Azure App Service. The application starts and API routes return JSON correctly, but all pages render as unstyled HTML with no client-side interactivity. What is missing?
Key takeaways
- Standalone output mode produces a minimal server bundle without
node_modules. Addoutput: 'standalone'tonext.config.tsand add native SDKs likeapplicationinsightstoserverExternalPackages. - Copy static files before zipping.
cp -r .next/static .next/standalone/.next/staticandcp -r public .next/standalone/publicare mandatory. This is the most common Next.js deployment failure on Azure App Service. - Azure Gov uses
.azurewebsites.us, not.azurewebsites.net. All URLs, CORS origins, and callback configurations must use the gov domain. - Port 8080 is the Azure App Service convention. Set both
PORTandWEBSITES_PORTto8080in App Service configuration. - Health checks should be simple. Return 200 if the process is alive. Do not check database connectivity in the health endpoint unless you want restart loops during transient DB outages.
- Secrets belong in Key Vault, not in App Service configuration.
NEXTAUTH_SECRET, database connection strings, and API keys should be fetched at runtime via Managed Identity.
What’s next
In the capstone lesson, you will automate this entire deployment process in an Azure Pipelines YAML pipeline: lint, test, build, Docker image, and deploy to Azure Gov — with approval gates, SAST scanning, and environment-specific variables.