# Deploying Next.js on Azure

> Source: https://agilitycms.com/docs/nextjs/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:

- **Agility CMS Next.js Starter** - [https://github.com/agility/agilitycms-nextjs-starter](https://github.com/agility/agilitycms-nextjs-starter) - Production-ready starter with App Router, TypeScript, and Tailwind CSS
- **Agility CMS Comprehensive Demo** - [https://github.com/agility/nextjs-demo-site-2025](https://github.com/agility/nextjs-demo-site-2025) - Full-featured demo with AI search, internationalization, and advanced features
- **Your Own Agility Next.js Site** - Existing project already integrated with Agility CMS

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 Method | Best For | Next.js Features Support | Complexity |
|------------------|----------|-------------------------|------------|
| **Azure Static Web Apps** | Hybrid Next.js apps with SSR/ISR | Full (with managed backend) | Low |
| **Azure App Service** | Full-featured Node.js hosting | All features supported | Medium |
| **Docker** | Custom infrastructure, K8s | All features supported | Medium-High |

---

## Option 1: Azure Static Web Apps (Recommended for Most Cases)

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 22.x or later (Next.js 16 requires 20.9+; Node 18 and 20 are both EOL)
- Next.js 15.x or later (this guide assumes the App Router; Next.js 16 for the `proxy.ts` naming)
- 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](https://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):
   ```bash
   git clone https://github.com/agility/agilitycms-nextjs-starter.git
   cd agilitycms-nextjs-starter
   ```

   Or use the **Comprehensive Demo** for advanced features:
   ```bash
   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**:
   ```json
   {
     "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**:
   - Go to [Azure Portal](https://portal.azure.com)
   - Sign in with your Azure account

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:
   ```tsx
   // app/page.tsx
   import { connection } from 'next/server'

   export default async function Home() {
     await connection()   // opt out of prerendering for what follows

     const timeOnServer = new Date().toLocaleTimeString('en-US')

     return (
       <main>
         <div>
           Server time: <strong>{timeOnServer}</strong>
         </div>
       </main>
     )
   }
   ```

   > `unstable_noStore()` from `next/cache` used to be the way to do this. It is deprecated — use `connection()` from `next/server`. Under [Cache Components](/docs/nextjs/caching-with-next-js-and-agility) this matters: an unstable value like `new Date()` outside a cached scope and without `connection()` fails the prerender rather than silently rendering per request.

2. Add route handlers:
   ```ts
   // app/api/currentTime/route.ts
   import { NextResponse, connection } from 'next/server'

   export async function GET() {
     await connection()
     const currentTime = new Date().toLocaleTimeString('en-US')
     return NextResponse.json({ message: `Current time is ${currentTime}.` })
   }
   ```

   > Don't reach for `export const dynamic = 'force-dynamic'` here. Route segment configs — `dynamic`, `revalidate`, `runtime`, `dynamicParams` — are **rejected** when `cacheComponents` is enabled. `connection()` is the replacement.

### Step 4: Optimize for Size (if >250 MB)

**Enable Standalone Output:**

```javascript
// next.config.mjs
export default {
  output: 'standalone',
}
```

**Update build script to copy static assets:**

```json
{
  "scripts": {
    "build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/"
  }
}
```

### Step 5: Configure Routing and the Proxy

> **Coming from an older project?** Next.js 16 renamed `middleware.ts` to `proxy.ts`, and the exported function from `middleware` to `proxy`. Everything below uses the current name. To migrate an existing file, run `npx @next/codemod@canary middleware-to-proxy .` — the behaviour is unchanged. See the [Next.js proxy reference](https://nextjs.org/docs/app/api-reference/file-conventions/proxy).

**How the 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, route handlers) are proxied to the managed backend App Service
- **Your `proxy.ts`** runs on the backend App Service instance before requests reach your Next.js application

**Critical configuration**: Static Web Apps validates a deployment by requesting `/.swa/health.html`. You must exclude `.swa` routes from the proxy matcher and from custom routing, or every deployment fails its health check:

```typescript
// proxy.ts
import { NextResponse, type 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/ (route handlers, handled separately)
     */
    '/((?!\\.swa|_next/static|api/).*)',
  ],
}

export function proxy(request: NextRequest) {
  // Runs on the backend App Service instance
  return NextResponse.next()
}
```

> ⚠️ **Two matcher gotchas.** Escape the dot — `\\.swa` in the string literal, which compiles to the regex `\.`. Leave it unescaped and `.` matches any character. And give each **directory** exclusion a trailing slash: the lookahead is an unanchored prefix test, so a bare `api` also excludes `/api-reference` and anything else merely starting with those letters. Exact filenames like `favicon\\.ico` must **not** get a slash.

**Important notes:**
- The proxy executes on the **backend App Service**, not at the CDN edge
- It can read request headers and cookies, and perform redirects
- For preview mode with Agility CMS, the proxy handles the `agilitypreviewkey` parameter
- Route rewrites within Next.js belong in `next.config`, not `staticwebapp.config.json`

**For redirects in `next.config.mjs`:**

```javascript
export default {
  async redirects() {
    return [
      {
        source: '/((?!.swa).*)/old-route',
        destination: '/new-route',
        permanent: false,
      },
    ]
  },
};
```

**Reference**: [Next.js Proxy Documentation](https://nextjs.org/docs/app/api-reference/file-conventions/proxy) | [Azure Static Web Apps Next.js Support](https://learn.microsoft.com/en-us/azure/static-web-apps/nextjs)

### 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**:
   ```yaml
   - 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
   ```
5. **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 22.x or later (Next.js 16 requires 20.9+; Node 18 and 20 are both EOL)
- 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](https://github.com/agility/agilitycms-nextjs-starter) or [Comprehensive Demo](https://github.com/agility/nextjs-demo-site-2025), you can skip the `create-next-app` step and proceed directly to Step 2.

```bash
# 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:
   ```bash
   # 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
   <?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):
   ```javascript
   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 = 22.11.0
```

**Step 4: Build the Application**

```bash
npm run build
```

This creates the `.next` folder required for deployment.

**Step 5: Deploy via Local Git**

```bash
# 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 main
```

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`:

```json
{
  "scripts": {
    "start": "node_modules/next/dist/bin/next start"
  }
}
```

**GitHub Actions Workflow:**

```yaml
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@v5

    - name: Set up Node.js
      uses: actions/setup-node@v5
      with:
        node-version: '22.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@v4
      with:
        name: node-app
        path: next.zip

  deploy:
    runs-on: ubuntu-latest
    needs: build

    steps:
    - name: Download artifact
      uses: actions/download-artifact@v4
      with:
        name: node-app

    - name: Deploy to Azure Web App
      uses: azure/webapps-deploy@v3
      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:**

```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: '22.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:
```bash
# 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.

### Proxy Configuration for App Service

**How the proxy works in Azure App Service:**

App Service runs Next.js as a standard Node.js application:
- **All requests** (static and dynamic) are handled by your Next.js server
- **`proxy.ts`** runs directly in your Node.js process before requests reach route handlers
- **No CDN layer in front** — App Service forwards requests straight to your Next.js server
- **Full control** over proxy execution

```typescript
// proxy.ts
import { NextResponse, type 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).*)',
  ],
}

export function proxy(request: NextRequest) {
  // Runs in your Node.js process, with full access to headers and cookies

  // Example: handle Agility CMS preview mode
  const previewKey = request.nextUrl.searchParams.get('agilitypreviewkey')
  if (previewKey) {
    // Handle preview logic
  }

  return NextResponse.next()
}
```

> ⚠️ The negative-lookahead pattern does **not** match the bare root `/` — list it explicitly, as above, or your home page skips the proxy entirely.

**Important notes:**
- The proxy runs in your Node.js process, in-band with the request
- No `.swa` exclusions needed here, unlike Static Web Apps
- Because App Service serves every request from your own process, the proxy always runs — including on pages a CDN would otherwise serve from cache. On a CDN-fronted host that is not true, and preview links need `beforeFiles` rewrites as well; see [Handling Preview URLs & Request Lifecycle](/docs/nextjs/handling-preview-urls-request-lifecycle).

**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:

```javascript
// 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](https://nextjs.org/docs/app/api-reference/file-conventions/proxy) | [Next.js Deployment Options](https://nextjs.org/docs/app/getting-started/deploying)

---

## 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.

```dockerfile
FROM node:24-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

```javascript
// next.config.mjs
export default {
  output: 'standalone',
}
```

### Step 3: Build and Test Locally

```bash
# 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

```bash
# 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

```bash
# 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)

```yaml
# 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:
```bash
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:
```bash
kubectl apply -f deployment.yaml
```

### Azure DevOps with Docker

```yaml
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:
```json
{
  "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**: the proxy or your routing is blocking `/.swa/health.html`

**Solution**: Exclude `.swa` routes from the proxy matcher (see the Static Web Apps section above). Ensure your `proxy.ts` excludes them — and note the escaped dot:

```typescript
export const config = {
  matcher: ['/((?!\\.swa).*)'],
}
```

---

## Performance Optimization

### 1. Enable Standalone Output

```javascript
// next.config.mjs
export default {
  output: 'standalone',
}
```

### 2. Configure Caching

For App Service, use Azure CDN for static assets:
```javascript
// next.config.mjs
export default {
  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 happens at the Edge, on Agility's CDN. It does not need to happen on your web server — and on Azure, routing every image through the Next.js optimizer means paying App Service compute for work the CDN already did.

Use `AgilityPic` from `@agility/nextjs`. It renders a plain `<picture>` tag, needs no configuration, requires no JavaScript, and lets Agility's CDN pick the format and size:

```jsx
import { AgilityPic } from "@agility/nextjs";

<AgilityPic
  src={fields.image.url}
  alt={fields.image.label}
  width={fields.image.width || 768}
  height={fields.image.height || 512}
/>
```

**No `next.config` image configuration is needed for Agility images.**

If you have *non-Agility* images that genuinely need `next/image`, allow their hosts with `remotePatterns` — not `domains`, which is deprecated:

```js
// next.config.mjs
export default {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "your-cdn.azureedge.net" },
    ],
    formats: ["image/avif", "image/webp"],
  },
}
```

### 4. Enable Compression

For App Service, compression is enabled by default. For Docker:
```javascript
// next.config.mjs
export default {
  compress: true,
}
```

---

## Monitoring and Logging

### Azure Application Insights

**For Static Web Apps:**
- Automatically enabled with managed backend

**For App Service:**
```bash
# Enable Application Insights
az webapp config appsettings set --name myapp \
  --resource-group myResourceGroup \
  --settings APPINSIGHTS_INSTRUMENTATIONKEY=<your-key>
```

**For Docker:**
Add to Dockerfile:
```dockerfile
ENV APPLICATIONINSIGHTS_CONNECTION_STRING=<connection-string>
```

### Log Streaming

**App Service:**
```bash
# View live logs
az webapp log tail --name myapp --resource-group myResourceGroup
```

**Container Apps:**
```bash
# View container logs
az containerapp logs show --name myapp --resource-group myResourceGroup
```

---

## Security Best Practices

### 1. Use Managed Identity

```bash
# 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:
```bash
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:
```bash
# 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

```javascript
// next.config.mjs
export default {
  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

| Scenario | Recommended Solution |
|----------|---------------------|
| Small app, mostly static with some SSR | **Static Web Apps Free** |
| Need custom backend services | **Static Web Apps Standard** |
| Complex app with background jobs | **App Service** |
| Microservices architecture | **Docker + AKS** |
| Need VNet integration | **App Service** or **Container Apps** |
| Maximum control over infrastructure | **Docker** |
| Lowest cost for simple apps | **Static Web Apps** |
| CI/CD with GitHub | **Static Web Apps** (easiest) |
| CI/CD with Azure DevOps | **App Service** or **Docker** |

---

## Additional Resources

- [Next.js Documentation](https://nextjs.org/docs)
- [Azure Static Web Apps Documentation](https://learn.microsoft.com/en-us/azure/static-web-apps/)
- [Azure App Service Documentation](https://learn.microsoft.com/en-us/azure/app-service/)
- [Azure Container Instances Documentation](https://learn.microsoft.com/en-us/azure/container-instances/)
- [Agility CMS + Next.js Guide](https://agilitycms.com/docs/nextjs)

---

## Quick Start Commands

### Static Web Apps
```bash
# Run your app the normal way for local development
npm run dev
```

> The SWA CLI's local emulation is **not supported** for hybrid Next.js apps (see Limitations above), so `swa start` won't faithfully reproduce your deployment. Develop with `next dev` and test proxy/routing behaviour on a real deployment.

### App Service
```bash
# Deploy using Azure CLI
az webapp up --name myapp --runtime "NODE:22-lts"
```

### Docker
```bash
# 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.
