Deploying Next.js on Azure

Overview

This guide covers deploying Next.js applications to Azure using the three most common deployment targets: Azure Static Web Apps, Azure App Service, and Docker containers. Each approach has different capabilities and trade-offs.

Benefits of Deploying Next.js to Azure

Deploying your Agility CMS Next.js site to Azure provides several advantages:

  • Full Next.js Feature Support - Supports Static Site Generation (SSG), Server-Side Rendering (SSR), Incremental Static Regeneration (ISR), and React Server Components
  • Preview Mode - Full support for Agility CMS preview mode to review draft content before publishing
  • Scalability - Auto-scaling capabilities to handle traffic spikes
  • Global CDN - Built-in content delivery network for fast global performance
  • Integrated CI/CD - Seamless GitHub Actions integration for automated deployments
  • Cost-Effective - Free tier available for Static Web Apps, pay-as-you-go for App Service
  • Enterprise-Grade - Security, monitoring, and compliance features built-in

Starting Point

Most developers deploying Agility CMS websites to Azure will be starting from an existing Next.js project:

These starters already include proper Next.js configuration, Agility CMS integration, and the necessary build scripts. You'll still need to clone the repository, install dependencies, and configure your environment variables, but you won't need to set up Next.js from scratch or configure Agility CMS integration manually.

Quick Comparison

Deployment MethodBest ForNext.js Features SupportComplexity
Azure Static Web AppsHybrid Next.js apps with SSR/ISRFull (with managed backend)Low
Azure App ServiceFull-featured Node.js hostingAll features supportedMedium
DockerCustom infrastructure, K8sAll features supportedMedium-High

Azure Static Web Apps provides the easiest deployment path with built-in support for Next.js hybrid rendering, including Server-Side Rendering (SSR), React Server Components, and API routes.

Supported Features

  • ✅ Static Site Generation (SSG)
  • ✅ Server-Side Rendering (SSR)
  • ✅ React Server Components
  • ✅ API Routes
  • ✅ Incremental Static Regeneration (ISR)
  • ✅ Image Optimization (Note: For Agility CMS websites, ALL image optimization is handled at the Edge and does not require server-side processing)
  • ⚠️ Image caching (Note: For Agility CMS websites, ALL image caching is handled at the Edge via Agility's CDN, so server-side image caching is not needed)

Limitations

  • Maximum app size: 250 MB (use standalone output if needed)
  • No linked APIs (Azure Functions, App Service, Container Apps)
  • SWA CLI local emulation not supported
  • Some staticwebapp.config.json features unsupported (navigation fallback, certain rewrites)

Prerequisites

  • Azure account with active subscription
  • GitHub account
  • Node.js 18.17.1 or later
  • Next.js 12.x or later
  • Agility CMS instance with API credentials (see below if you need to set one up)

Get Your Agility CMS Credentials

If you don't already have an Agility CMS instance set up:

  1. Sign up for Agility CMS - Create a free account at agilitycms.com
  2. Create an Instance - Create a new instance (you can start with a Blog Starter template)
  3. Get API Keys - Navigate to Settings > API Keys in your Agility CMS dashboard
  4. Copy your credentials:
    • GUID (Instance ID)
    • Live API Key (for production)
    • Preview API Key (for development/preview)
    • Security Key (for webhooks)

You'll need these credentials for both local development and Azure deployment configuration.

Step 1: Prepare Your Agility Next.js Project

Start with an Agility CMS Next.js Starter:

  1. Clone the Agility CMS Next.js Starter (recommended):

    git clone https://github.com/agility/agilitycms-nextjs-starter.git
    cd agilitycms-nextjs-starter
    

    Or use the Comprehensive Demo for advanced features:

    git clone https://github.com/agility/nextjs-demo-site-2025.git
    cd nextjs-demo-site-2025
    
  2. Verify your package.json has the required scripts:

    {
      "scripts": {
        "dev": "next dev",
        "build": "next build",
        "start": "next start"
      }
    }
    

    Note: The Agility starters already include proper Next.js configuration for Azure deployment, including image optimization settings that work with Agility's Edge CDN.

  3. If using your own existing Agility Next.js project, ensure it has the same script configuration above.

Step 2: Create Static Web App in Azure Portal

  1. Navigate to Azure Portal:

  2. Create a New Resource:

    • Click Create a resource (or use the search bar)
    • Search for "Static Web Apps"
    • Select Static Web Apps and click Create
  3. Basics Tab:

    • Subscription: Select your Azure subscription
    • Resource Group: Select an existing resource group or create a new one
    • Name: Enter a unique name for your Static Web App
    • Plan type: Select Free (for testing) or Standard (for production with linked backends)
    • Region: Select the Azure region closest to your users
  4. Deployment Details Tab:

    • Source: Select GitHub (or Azure DevOps if preferred)
    • Sign in with GitHub: Authenticate and authorize Azure to access your GitHub account
    • Organization: Select your GitHub organization or username
    • Repository: Select the repository containing your Next.js project
    • Branch: Select main (or your default branch)
    • Build Presets: Select Next.js
    • App location: / (or your app folder if your Next.js app is in a subdirectory)
    • API location: Leave empty (unless you have Azure Functions)
    • Output location: Leave empty (Next.js handles this automatically)
  5. Review + Create:

    • Review your configuration
    • Click Create to provision your Static Web App

Note: After creation, Azure will automatically create a GitHub Actions workflow file in your repository. The initial deployment may take a few minutes.

Step 3: Configure for Hybrid Rendering

Enable Server-Side Features:

  1. Add server-rendered data with Server Components:

    // app/page.tsx
    import { unstable_noStore as noStore } from 'next/cache';
    
    export default function Home() {
      noStore(); // Forces dynamic rendering
    
      const timeOnServer = new Date().toLocaleTimeString('en-US');
    
      return (
        <main>
          <div>
            Server time: <strong>{timeOnServer}</strong>
          </div>
        </main>
      );
    }
    
  2. Add API routes:

    // app/api/currentTime/route.ts
    import { NextResponse } from 'next/server';
    
    export const dynamic = 'force-dynamic';
    
    export async function GET() {
      const currentTime = new Date().toLocaleTimeString('en-US');
      return NextResponse.json({
        message: `Current time is ${currentTime}.`
      });
    }
    

Step 4: Optimize for Size (if >250 MB)

Enable Standalone Output:

// next.config.js
module.exports = {
  output: 'standalone',
}

Update build script to copy static assets:

{
  "scripts": {
    "build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/"
  }
}

Step 5: Configure Routing and Middleware/Proxy

Note: In Next.js 16+, middleware.ts has been renamed to proxy.ts (the functionality is the same). For Next.js 15 and earlier, use middleware.ts. This guide uses both terms interchangeably. See Next.js Proxy Documentation for migration details.

How Middleware/Proxy Works in Azure Static Web Apps:

Azure Static Web Apps uses a proxy architecture where:

  • Static assets are served directly from the CDN
  • Dynamic routes (SSR, API routes) are proxied to the managed backend App Service
  • Middleware/Proxy runs on the backend App Service instance before requests reach your Next.js application

Critical Configuration: Static Web Apps validates deployment by accessing /.swa/health.html. You must exclude .swa routes from middleware/proxy and custom routing:

// proxy.ts (Next.js 16+) or middleware.ts (Next.js 15 and earlier)
import { NextResponse, NextRequest } from 'next/server'

export const config = {
  matcher: [
    /*
     * Match all request paths except:
     * - .swa (Azure Static Web Apps health checks)
     * - _next/static (static assets)
     * - api (API routes handled separately)
     */
    '/((?!.swa|_next/static|api).*)',
  ],
}

// Next.js 16+ (proxy.ts)
export function proxy(request: NextRequest) {
  // Your proxy/middleware logic here
  // This runs on the backend App Service instance
  return NextResponse.next()
}

// Next.js 15 and earlier (middleware.ts)
// export function middleware(request: NextRequest) {
//   return NextResponse.next()
// }

Important Notes:

  • Middleware/Proxy executes on the backend App Service, not the CDN edge
  • Middleware/Proxy can access request headers, cookies, and perform redirects
  • For preview mode with Agility CMS, middleware/proxy handles the agilitypreviewkey parameter
  • Route rewrites within Next.js should be configured in next.config.js, not staticwebapp.config.json

For redirects in next.config.js:

module.exports = {
  async redirects() {
    return [
      {
        source: '/((?!.swa).*)/old-route',
        destination: '/new-route',
        permanent: false,
      },
    ]
  },
};

Reference: Next.js Proxy Documentation (Next.js 16+) | Next.js Middleware Documentation (Next.js 15 and earlier) | Azure Static Web Apps Next.js Support

Step 6: Set Environment Variables

Environment variables are needed at both build time and runtime:

  1. In GitHub Repository (for build):

    • Go to Settings → Secrets → Actions
    • Add secrets:
      • AGILITY_GUID
      • AGILITY_API_FETCH_KEY
      • AGILITY_API_PREVIEW_KEY
      • AGILITY_SECURITY_KEY
      • AGILITY_LOCALES (e.g., en-us)
  2. In Azure Static Web App (for runtime):

    • Go to Configuration → Environment variables
    • Add the same Agility CMS variables
  3. In GitHub Actions workflow:

    - name: Build And Deploy
      uses: Azure/static-web-apps-deploy@v1
      with:
        azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
        action: "upload"
        app_location: "/"
      env:
        AGILITY_GUID: ${{ secrets.AGILITY_GUID }}
        AGILITY_API_FETCH_KEY: ${{ secrets.AGILITY_API_FETCH_KEY }}
        AGILITY_API_PREVIEW_KEY: ${{ secrets.AGILITY_API_PREVIEW_KEY }}
        AGILITY_SECURITY_KEY: ${{ secrets.AGILITY_SECURITY_KEY }}
       AGILITY_LOCALES: ${{ secrets.AGILITY_LOCALES }}
    

### Troubleshooting Build Errors

If you encounter build errors during deployment, check the following:

1. **Verify API Credentials** - Ensure your `AGILITY_GUID` and API keys are correct in GitHub Secrets
2. **Check Environment Variables** - Make sure all required Agility CMS variables are set in both GitHub Secrets and Azure Configuration
3. **Review Build Logs** - Check the GitHub Actions logs or Azure deployment logs for specific error messages
4. **Local Build Test** - Run `npm run build` locally to catch build errors before deploying:
 ```bash
 npm run build
  1. Verify .env.local - If testing locally, ensure your .env.local file exists and contains all required variables

Common issues:

  • "Invalid API Key" - Double-check your AGILITY_API_FETCH_KEY matches your Live API Key from Agility CMS
  • "Instance not found" - Verify your AGILITY_GUID is correct
  • "Build timeout" - Your app may exceed the 250 MB limit; enable standalone output (see Step 4)

Step 7: Bring Your Own Backend (Optional)

For better performance and control, link a dedicated backend:

  1. Go to Static Web App → Settings → APIs
  2. Select Configure linked backend
  3. Create or select an App Service Plan (S1 SKU or higher)
  4. Click Link

Note: Linked backends require Standard plan or above


Option 2: Azure App Service

Azure App Service provides full Node.js hosting with complete control over the runtime environment. Use this when you need features not available in Static Web Apps or want more control.

Supported Features

  • ✅ All Next.js features
  • ✅ Custom server configurations
  • ✅ Full environment control
  • ✅ Advanced scaling options
  • ✅ VNet integration

Prerequisites

  • Azure account
  • Node.js 18.x or later
  • App Service Plan (B1 or higher recommended)

Deployment Options

Option A: Deploy to App Service Linux

Step 1: Local Setup

Note: If you're deploying from the Agility CMS Next.js Starter or Comprehensive Demo, you can skip the create-next-app step and proceed directly to Step 2.

# If starting from scratch (not recommended for Agility CMS projects)
npx create-next-app@latest my-nextjs-app
cd my-nextjs-app

# OR clone and use an Agility starter:
git clone https://github.com/agility/agilitycms-nextjs-starter.git
cd agilitycms-nextjs-starter

Step 2: Add Required Files

Next.js on App Service requires specific configuration files:

  1. Create .env.local for local development:

    # Your Agility CMS Instance credentials
    AGILITY_GUID=<your-guid>
    AGILITY_API_FETCH_KEY=<your-live-api-key>
    AGILITY_API_PREVIEW_KEY=<your-preview-api-key>
    AGILITY_SECURITY_KEY=<your-security-key>
    AGILITY_LOCALES=en-us
    
  2. Create web.config (for IIS on Windows) or skip for Linux:

    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <system.webServer>
        <webSocket enabled="false" />
        <handlers>
          <add name="iisnode" path="server.js" verb="*" modules="iisnode"/>
        </handlers>
        <rewrite>
          <rules>
            <rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true">
              <match url="^server.js\\/debug[\\/]?" />
            </rule>
            <rule name="StaticContent">
              <action type="Rewrite" url="public{REQUEST_URI}"/>
            </rule>
            <rule name="DynamicContent">
              <conditions>
                <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True"/>
              </conditions>
              <action type="Rewrite" url="server.js"/>
            </rule>
          </rules>
        </rewrite>
        <security>
          <requestFiltering>
            <hiddenSegments>
              <remove segment="bin"/>
            </hiddenSegments>
          </requestFiltering>
        </security>
        <httpErrors existingResponse="PassThrough" />
        <iisnode watchedFiles="web.config;*.js"/>
      </system.webServer>
    </configuration>
    
  3. Create server.js (critical for App Service):

    const { createServer } = require("http");
    const next = require("next");
    
    const port = process.env.PORT || 8080;
    const dev = process.env.NODE_ENV !== "production";
    const app = next({ dev });
    const handle = app.getRequestHandler();
    
    app.prepare().then(() => {
      createServer((req, res) => {
        handle(req, res);
      }).listen(port, (err) => {
        if (err) throw err;
        console.log(`> Ready on http://localhost:${port}`);
      });
    });
    

Step 3: Set Node Version

Add to your App Service Configuration:

Application Setting:
WEBSITE_NODE_DEFAULT_VERSION = 18.17.1

Step 4: Build the Application

npm run build

This creates the .next folder required for deployment.

Step 5: Deploy via Local Git

# Initialize git repository
git init
git add .
git commit -m "Initial commit"

# Add Azure remote (get URL from Portal → Deployment Center)
git remote add azure https://<sitename>.scm.azurewebsites.net:443/<sitename>.git

# Push to Azure
git push azure master

Oryx will automatically detect Next.js and build your application.

Option B: Deploy with GitHub Actions

Critical Fix for Symlink Issue:

When deploying via GitHub Actions or Azure DevOps (ZipDeploy), NPM symlinks may break. Update your package.json:

{
  "scripts": {
    "start": "node_modules/next/dist/bin/next start"
  }
}

GitHub Actions Workflow:

name: Build and Deploy Next.js to Azure App Service

on:
  push:
    branches: [ main ]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2

    - name: Set up Node.js
      uses: actions/setup-node@v1
      with:
        node-version: '18.x'

    - name: npm install and build
      run: |
        npm install
        npm run build --if-present
      env:
        AGILITY_GUID: ${{ secrets.AGILITY_GUID }}
        AGILITY_API_FETCH_KEY: ${{ secrets.AGILITY_API_FETCH_KEY }}
        AGILITY_API_PREVIEW_KEY: ${{ secrets.AGILITY_API_PREVIEW_KEY }}
        AGILITY_SECURITY_KEY: ${{ secrets.AGILITY_SECURITY_KEY }}
        AGILITY_LOCALES: ${{ secrets.AGILITY_LOCALES }}

    - name: Zip all files
      # IMPORTANT: Include .next hidden folder
      run: zip -r next.zip ./* .next

    - name: Upload artifact
      uses: actions/upload-artifact@v2
      with:
        name: node-app
        path: next.zip

  deploy:
    runs-on: ubuntu-latest
    needs: build

    steps:
    - name: Download artifact
      uses: actions/download-artifact@v2
      with:
        name: node-app

    - name: Deploy to Azure Web App
      uses: azure/webapps-deploy@v2
      with:
        app-name: 'your-app-name'
        publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
        package: next.zip

    - name: Cleanup
      run: rm next.zip

Option C: Deploy with Azure DevOps

Azure Pipelines YAML:

trigger:
  - main

variables:
  azureSubscription: 'your-subscription'
  webAppName: 'your-app-name'
  vmImageName: 'ubuntu-latest'

stages:
- stage: Build
  jobs:
  - job: Build
    pool:
      vmImage: $(vmImageName)

    steps:
    - task: NodeTool@0
      inputs:
        versionSpec: '18.x'
      displayName: 'Install Node.js'

    - script: |
        npm install
        npm run build --if-present
      displayName: 'npm install and build'
      env:
        AGILITY_GUID: $(AGILITY_GUID)
        AGILITY_API_FETCH_KEY: $(AGILITY_API_FETCH_KEY)
        AGILITY_API_PREVIEW_KEY: $(AGILITY_API_PREVIEW_KEY)
        AGILITY_SECURITY_KEY: $(AGILITY_SECURITY_KEY)
        AGILITY_LOCALES: $(AGILITY_LOCALES)

    - task: ArchiveFiles@2
      displayName: 'Archive files'
      inputs:
        rootFolderOrFile: '$(System.DefaultWorkingDirectory)'
        includeRootFolder: false
        archiveType: zip
        archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
        replaceExistingArchive: true

    - publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
      artifact: drop

- stage: Deploy
  dependsOn: Build
  jobs:
  - deployment: Deploy
    environment: 'production'
    pool:
      vmImage: $(vmImageName)
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureWebApp@1
            displayName: 'Deploy to Azure App Service'
            inputs:
              azureSubscription: $(azureSubscription)
              appType: webAppLinux
              appName: $(webAppName)
              package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip

Environment Variables for App Service

Set environment variables in two places:

  1. Azure Portal → App Service → Configuration → Application settings:

    AGILITY_GUID=<your-guid>
    AGILITY_API_FETCH_KEY=<your-fetch-key>
    AGILITY_API_PREVIEW_KEY=<your-preview-key>
    AGILITY_SECURITY_KEY=<your-security-key>
    AGILITY_LOCALES=en-us
    
  2. GitHub/DevOps workflow (for build-time variables) - shown in examples above

Viewing Logs (Log Stream)

App Service Log Stream is useful for debugging deployment issues and monitoring your application in real-time:

  1. Go to your App Service in Azure Portal
  2. Navigate to Monitoring → Log stream (or use Development Tools → Log stream)
  3. View real-time logs from your application

You can also use Azure CLI:

# View live logs
az webapp log tail --name myapp --resource-group myResourceGroup

Tip: Enable Application Logging in Configuration → General settings to see more detailed logs.

Middleware/Proxy Configuration for App Service

Note: In Next.js 16+, middleware.ts has been renamed to proxy.ts (the functionality is the same). For Next.js 15 and earlier, use middleware.ts. This guide uses both terms interchangeably.

How Middleware/Proxy Works in Azure App Service:

Azure App Service runs Next.js as a standard Node.js application:

  • All requests (static and dynamic) are handled by your Next.js server
  • Middleware/Proxy runs directly in your Node.js process before requests reach route handlers
  • No proxy layer - App Service forwards requests directly to your Next.js server
  • Full control - You have complete control over middleware/proxy execution

Middleware/Proxy Configuration:

// proxy.ts (Next.js 16+) or middleware.ts (Next.js 15 and earlier)
import { NextResponse, NextRequest } from 'next/server'

export const config = {
  matcher: [
    /*
     * Match all request paths except:
     * - _next/static (static files)
     * - _next/image (image optimization)
     * - favicon.ico (favicon file)
     */
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
}

// Next.js 16+ (proxy.ts)
export function proxy(request: NextRequest) {
  // Proxy/middleware runs in your Node.js process
  // Full access to request/response, headers, cookies, etc.

  // Example: Handle Agility CMS preview mode
  const previewKey = request.nextUrl.searchParams.get('agilitypreviewkey')
  if (previewKey) {
    // Handle preview logic
  }

  return NextResponse.next()
}

// Next.js 15 and earlier (middleware.ts)
// export function middleware(request: NextRequest) {
//   return NextResponse.next()
// }

Important Notes:

  • Middleware/Proxy executes synchronously in your Node.js process
  • No special route exclusions needed (unlike Static Web Apps)
  • Middleware/Proxy can perform redirects, rewrite URLs, set headers, etc.
  • For preview mode with Agility CMS, middleware/proxy handles the agilitypreviewkey parameter
  • All Next.js middleware/proxy features are fully supported

Custom Server Considerations:

If you're using a custom server.js, middleware/proxy still works normally. The custom server uses app.getRequestHandler(), which automatically processes middleware/proxy:

// server.js
const { createServer } = require("http");
const next = require("next");

const app = next({ dev });
const handle = app.getRequestHandler(); // This includes middleware/proxy processing

app.prepare().then(() => {
  createServer((req, res) => {
    handle(req, res); // Middleware/Proxy runs automatically
  }).listen(port);
});

Reference: Next.js Proxy Documentation (Next.js 16+) | Next.js Middleware Documentation (Next.js 15 and earlier) | Next.js Deployment Options


Option 3: Docker Deployment

Docker provides maximum flexibility and works with any container orchestration platform.

Supported Features

  • ✅ All Next.js features
  • ✅ Kubernetes compatibility
  • ✅ Multi-environment support
  • ✅ Custom infrastructure

Step 1: Create Dockerfile

Important: For Agility CMS projects, ensure environment variables are passed at build time and runtime.

FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies
COPY package.json package-lock.json* ./
RUN npm ci

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Accept build arguments for Agility CMS
ARG AGILITY_GUID
ARG AGILITY_API_FETCH_KEY
ARG AGILITY_API_PREVIEW_KEY
ARG AGILITY_SECURITY_KEY
ARG AGILITY_LOCALES

# Set environment variables for build
ENV AGILITY_GUID=$AGILITY_GUID
ENV AGILITY_API_FETCH_KEY=$AGILITY_API_FETCH_KEY
ENV AGILITY_API_PREVIEW_KEY=$AGILITY_API_PREVIEW_KEY
ENV AGILITY_SECURITY_KEY=$AGILITY_SECURITY_KEY
ENV AGILITY_LOCALES=$AGILITY_LOCALES

RUN npm run build

# Production image
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]

Step 2: Configure for Standalone Output

// next.config.js
module.exports = {
  output: 'standalone',
}

Step 3: Build and Test Locally

# Build the Docker image with Agility CMS build arguments
docker build -t nextjs-app \
  --build-arg AGILITY_GUID=<your-guid> \
  --build-arg AGILITY_API_FETCH_KEY=<your-fetch-key> \
  --build-arg AGILITY_API_PREVIEW_KEY=<your-preview-key> \
  --build-arg AGILITY_SECURITY_KEY=<your-security-key> \
  --build-arg AGILITY_LOCALES=en-us \
  .

# Run locally with Agility CMS environment variables
docker run -p 3000:3000 \
  -e AGILITY_GUID=<your-guid> \
  -e AGILITY_API_FETCH_KEY=<your-fetch-key> \
  -e AGILITY_API_PREVIEW_KEY=<your-preview-key> \
  -e AGILITY_SECURITY_KEY=<your-security-key> \
  -e AGILITY_LOCALES=en-us \
  nextjs-app

Step 4: Deploy to Azure

Option A: Azure Container Registry + App Service

# Login to Azure
az login

# Create container registry
az acr create --resource-group myResourceGroup \
  --name myregistry --sku Basic

# Build and push to ACR with Agility CMS build arguments
az acr build --registry myregistry \
  --image nextjs-app:latest \
  --build-arg AGILITY_GUID=<your-guid> \
  --build-arg AGILITY_API_FETCH_KEY=<your-fetch-key> \
  --build-arg AGILITY_API_PREVIEW_KEY=<your-preview-key> \
  --build-arg AGILITY_SECURITY_KEY=<your-security-key> \
  --build-arg AGILITY_LOCALES=en-us \
  .

# Create App Service with container and environment variables
az webapp create --resource-group myResourceGroup \
  --plan myAppServicePlan --name myapp \
  --deployment-container-image-name myregistry.azurecr.io/nextjs-app:latest

# Set Agility CMS environment variables
az webapp config appsettings set --name myapp \
  --resource-group myResourceGroup \
  --settings \
    AGILITY_GUID=<your-guid> \
    AGILITY_API_FETCH_KEY=<your-fetch-key> \
    AGILITY_API_PREVIEW_KEY=<your-preview-key> \
    AGILITY_SECURITY_KEY=<your-security-key> \
    AGILITY_LOCALES=en-us

Option B: Azure Container Instances

# Create container instance with Agility CMS environment variables
az container create --resource-group myResourceGroup \
  --name nextjs-app --image myregistry.azurecr.io/nextjs-app:latest \
  --dns-name-label nextjs-app-unique --ports 3000 \
  --environment-variables \
    AGILITY_GUID=<your-guid> \
    AGILITY_API_FETCH_KEY=<your-fetch-key> \
    AGILITY_LOCALES=en-us \
  --secure-environment-variables \
    AGILITY_API_PREVIEW_KEY=<your-preview-key> \
    AGILITY_SECURITY_KEY=<your-security-key>

Option C: Azure Kubernetes Service (AKS)

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nextjs-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nextjs-app
  template:
    metadata:
      labels:
        app: nextjs-app
    spec:
      containers:
      - name: nextjs-app
        image: myregistry.azurecr.io/nextjs-app:latest
        ports:
        - containerPort: 3000
        env:
        - name: AGILITY_GUID
          valueFrom:
            secretKeyRef:
              name: agility-secrets
              key: guid
        - name: AGILITY_API_FETCH_KEY
          valueFrom:
            secretKeyRef:
              name: agility-secrets
              key: fetch-key
        - name: AGILITY_API_PREVIEW_KEY
          valueFrom:
            secretKeyRef:
              name: agility-secrets
              key: preview-key
        - name: AGILITY_SECURITY_KEY
          valueFrom:
            secretKeyRef:
              name: agility-secrets
              key: security-key
        - name: AGILITY_LOCALES
          value: "en-us"
---
apiVersion: v1
kind: Service
metadata:
  name: nextjs-app
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 3000
  selector:
    app: nextjs-app

Create the Kubernetes secret first:

kubectl create secret generic agility-secrets \
  --from-literal=guid=<your-guid> \
  --from-literal=fetch-key=<your-fetch-key> \
  --from-literal=preview-key=<your-preview-key> \
  --from-literal=security-key=<your-security-key>

Deploy:

kubectl apply -f deployment.yaml

Azure DevOps with Docker

trigger:
  - main

variables:
  dockerRegistry: 'myregistry.azurecr.io'
  imageName: 'nextjs-app'
  tag: '$(Build.BuildId)'

stages:
- stage: Build
  jobs:
  - job: Build
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - task: Docker@2
      displayName: 'Build and Push'
      inputs:
        containerRegistry: '$(containerRegistry)'
        repository: '$(imageName)'
        command: 'buildAndPush'
        Dockerfile: '**/Dockerfile'
        tags: |
          $(tag)
          latest

- stage: Deploy
  jobs:
  - deployment: Deploy
    environment: 'production'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureWebAppContainer@1
            inputs:
              azureSubscription: 'mySubscription'
              appName: 'my-nextjs-app'
              containers: '$(dockerRegistry)/$(imageName):$(tag)'

Common Issues and Troubleshooting

1. "You do not have permission to view this directory or page" (App Service)

Cause: Missing web.config or server.js files

Solution: Add both files to your root directory (see App Service section above)

2. "next: command not found" (GitHub Actions/DevOps)

Cause: NPM symlinks broken during ZipDeploy

Solution: Update package.json start script:

{
  "scripts": {
    "start": "node_modules/next/dist/bin/next start"
  }
}

3. "Could not find a production build in '.next' directory"

Cause: .next folder not included in deployment

Solution:

  • For GitHub Actions: Include .next in zip: zip -r next.zip ./* .next
  • For Oryx: Ensure build script exists in package.json

4. Application timing out at container startup

Cause: Port configuration mismatch

Solution: Use port 8080 (default) or ensure PORT environment variable is set correctly

5. Deployment taking too long (GitHub Actions)

Cause: Too many small files being uploaded individually

Solution: Zip artifacts before upload (see GitHub Actions examples above)

6. Health check failing (Static Web Apps)

Cause: Middleware/Proxy or routing blocking /.swa/health.html

Solution: Exclude .swa routes from middleware/proxy matcher (see Static Web Apps section). Ensure your proxy.ts (Next.js 16+) or middleware.ts (Next.js 15 and earlier) excludes .swa routes:

export const config = {
  matcher: ['/((?!.swa).*)'],
}

Performance Optimization

1. Enable Standalone Output

// next.config.js
module.exports = {
  output: 'standalone',
}

2. Configure Caching

For App Service, use Azure CDN for static assets:

// next.config.js
module.exports = {
  assetPrefix: process.env.NODE_ENV === 'production'
    ? 'https://your-cdn.azureedge.net'
    : '',
}

Important for Agility CMS: ALL image caching for Agility websites is handled at the Edge via Agility's CDN. You do not need to configure server-side image caching for Agility CMS images.

3. Optimize Images

Important for Agility CMS: ALL image optimization for Agility websites is done at the Edge (via Agility's CDN) and does NOT need to happen on the web server. The Next.js Image component will work with Agility images, but you should configure it to use Agility's CDN domains and can disable server-side optimization.

// next.config.js
module.exports = {
  images: {
    // Use Agility CMS CDN domains
    domains: ['cdn.agilitycms.com', 'your-cdn.azureedge.net'],
    formats: ['image/avif', 'image/webp'],
    // Optional: Disable Next.js image optimization since Agility handles it at the Edge
    // unoptimized: true, // Uncomment if you want to rely entirely on Agility's Edge optimization
  },
}

Using the Next.js Image component with Agility CMS:

import Image from 'next/image';

// Agility images are already optimized and cached at the Edge
<Image
  src={agilityImage.url} // Direct URL from Agility CMS
  alt={agilityImage.label}
  width={agilityImage.width}
  height={agilityImage.height}
  // No need for server-side optimization or caching - Agility handles both at the Edge
/>

4. Enable Compression

For App Service, compression is enabled by default. For Docker:

// next.config.js
module.exports = {
  compress: true,
}

Monitoring and Logging

Azure Application Insights

For Static Web Apps:

  • Automatically enabled with managed backend

For App Service:

# Enable Application Insights
az webapp config appsettings set --name myapp \
  --resource-group myResourceGroup \
  --settings APPINSIGHTS_INSTRUMENTATIONKEY=<your-key>

For Docker: Add to Dockerfile:

ENV APPLICATIONINSIGHTS_CONNECTION_STRING=<connection-string>

Log Streaming

App Service:

# View live logs
az webapp log tail --name myapp --resource-group myResourceGroup

Container Apps:

# View container logs
az containerapp logs show --name myapp --resource-group myResourceGroup

Security Best Practices

1. Use Managed Identity

# Enable managed identity for App Service
az webapp identity assign --name myapp --resource-group myResourceGroup

2. Implement Authentication

Static Web Apps has built-in authentication providers:

  • Azure AD
  • GitHub
  • Twitter

App Service use Easy Auth:

az webapp auth update --name myapp --resource-group myResourceGroup \
  --enabled true --action LoginWithAzureActiveDirectory

3. Secure Environment Variables

  • Store Agility CMS secrets in Azure Key Vault
  • Reference secrets in App Service:
@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/agility-security-key/)
@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/agility-api-preview-key/)

Example for creating secrets in Key Vault:

# Create Key Vault
az keyvault create --name myvault --resource-group myResourceGroup

# Store Agility CMS secrets
az keyvault secret set --vault-name myvault \
  --name agility-security-key --value <your-security-key>
az keyvault secret set --vault-name myvault \
  --name agility-api-preview-key --value <your-preview-key>

# Grant App Service access to Key Vault
az keyvault set-policy --name myvault \
  --object-id $(az webapp identity show --name myapp \
    --resource-group myResourceGroup --query principalId -o tsv) \
  --secret-permissions get

4. Configure Security Headers

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'X-Frame-Options',
            value: 'DENY',
          },
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff',
          },
          {
            key: 'Referrer-Policy',
            value: 'origin-when-cross-origin',
          },
        ],
      },
    ]
  },
}

Cost Optimization

Static Web Apps

  • Free tier: Includes 100GB bandwidth/month
  • Standard tier: $9/month + bandwidth

App Service

  • B1: ~$13/month (basic, good for dev/test)
  • P1V3: ~$117/month (production, auto-scale)

Docker/AKS

  • Most flexible but requires more management
  • AKS node pools start at ~$70/month

Recommendation: Start with Static Web Apps Free tier, upgrade to Standard when you need linked backends or more bandwidth.


Decision Matrix

ScenarioRecommended Solution
Small app, mostly static with some SSRStatic Web Apps Free
Need custom backend servicesStatic Web Apps Standard
Complex app with background jobsApp Service
Microservices architectureDocker + AKS
Need VNet integrationApp Service or Container Apps
Maximum control over infrastructureDocker
Lowest cost for simple appsStatic Web Apps
CI/CD with GitHubStatic Web Apps (easiest)
CI/CD with Azure DevOpsApp Service or Docker

Additional Resources


Quick Start Commands

Static Web Apps

# Install Azure Static Web Apps CLI
npm install -g @azure/static-web-apps-cli

# Run locally with SWA CLI
swa start http://localhost:3000

App Service

# Deploy using Azure CLI
az webapp up --name myapp --runtime "NODE:18-lts"

Docker

# Build and run
docker build -t nextjs-app .
docker run -p 3000:3000 nextjs-app

This guide covers the essential deployment patterns for Next.js on Azure. Choose the approach that best fits your application's requirements, team expertise, and budget constraints.