Deploy NextJS 14+ app to Linux Azure App Service
Tags: webdev, azure, nextjs, devops
Published: 2023-11-10 12:07:35

NextJS is a fantastic framework for building React apps. It's easy to get started with and has a great developer experience. However it is really optimized to be deployed to Vercel, which is understandable given the good folks at Vercel created NextJS. I prefer to use Azure for my application deployments and in this guide I will walk you through the different changes that need to be made to your NextJS app to ensure it will run on Azure.

All deployments will be done using GitHub actions, so you will need to have a GitHub account and a repository with your NextJS app. I also assume you have the proper permissions to create Azure resources and have the Azure CLI installed. If you need help connecting your GitHub Action to Azure you can review my previous article [Connect GitHub Actions to Azure using OpenID Connect](https://paulriviera.com/articles/2023-11-9-Connect%20GitHub%20Actions%20to%20Azure%20using%20OpenID%20Connect) which will walk you through it.

# Getting Started

I will not provide a guide for creating a NextJS app, but you can follow the [official guide](https://nextjs.org/docs/getting-started) to get started. Once you have your app created you can follow the steps below to get it ready for deployment to Azure.

***As of writing this NextJS is at version 14.0.1, so I will be using that version for this guide. If you are using a different version you may need to make some adjustments.***

## Step 1: Provision the Azure App Service

Like all Azure resources we will create a resource group to contain our app service and then create the app service. The following script will create a resource group, an app service plan, and an app service. You will need to replace the values in the script with your own.

```powershell
$RESOURCE_GROUP_NAME = "<Name of Resource Group>"
$LOCATION = "<Location of Resource Group>"

$APP_SERVICE_PLAN_NAME = "<Name of App Service Plan>"
$APP_SERVICE_PLAN_SKU = "<SKU of App Service Plan>"

$WEB_APP_NAME = "<Name of Web App>"
$WEB_APP_RUNTIME = "NODE:18-lts" # This is not the latest version of Node, but it is the version supported by Azure App Service at the time of writing this article.

az group create `
    --name $RESOURCE_GROUP_NAME `
    --location $LOCATION

az appservice plan create `
    --resource-group $RESOURCE_GROUP_NAME `
    --location $LOCATION `
    --is-linux `
    --name $APP_SERVICE_PLAN_NAME `
    --sku $APP_SERVICE_PLAN_SKU

az webapp create `
    --resource-group $RESOURCE_GROUP_NAME `
    --plan $APP_SERVICE_PLAN_NAME `
    --name $WEB_APP_NAME `
    --https-only $true `
    --runtime $WEB_APP_RUNTIME `
    --assign-identity [system]
```

## Step 2: Configure the Azure App Service for NextJS

The app service needs to be told how to run the NextJS app, to accomplish this we will set a startup command that will be run once the deployment is complete. Normally you would just run a `npm start` on the build output but in the next step we will modify the output to require some changes to the startup command.

```powershell
$WEB_APP_NAME = "<Name of Web App>"
$RESOURCE_GROUP_NAME = "<Name of Resource Group>"

az webapp config set `
    --resource-group $RESOURCE_GROUP_NAME `
    --name $WEB_APP_NAME `
    --startup-file "node .next/standalone/server.js"
```

***Note that the parameter is `--startup-file` and not `--startup-command`, while I don't like this naming, this naming it is what the Azure CLI provides..***

## Step 2: Configure the NextJS App output to be Standalone

Since NextJS is optimized for Vercel the default build output will not work on Azure, it can be deployed but the required files will not exist. To fix this we need to change the build output to be standalone. This will ensure all the required files are included in the build output.

All you need to do for this is update the output in `next.config.js` to be *standalone*.

```javascript
const nextConfig = {
    output: "standalone"
}

module.exports = nextConfig
```

Once that is done you can build your app and your will notice the `.next` build folder will now contain a `standalone` folder. This folder will be the focus of your deployment to Azure, however it still has dependencies on the `.next/server` folder and we will have to do a little more work to get it ready for Azure, but this will be in the pipeline.

## Step 3: Create a GitHub Action to Deploy to Azure

In this step we will start with the initial GitHub Action workflow and slowly add additional steps. When we are done we will have a workflow that will build the NextJS app, move the required files to the `standalone` folder, and then deploy the app to Azure.

### Step 3.1: Create the initial GitHub Action workflow

The following workflow will be the starting point for our GitHub Action. It will be triggered on a push to the `main` branch and will run on an Ubuntu VM. It will also install NodeJS and the required dependencies for the NextJS app. The last step will login to Azure using the OIDC authentication method. Replace the login step if you're using a different method to connect to Azure.

```yml
name: Deploy to Azure

on:
  push:
    branches: ["main"]

  workflow_dispatch:

permissions:
  id-token: write # This is required for requesting the JWT
  contents: read # This is required for actions/checkout

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout # Checked out your repository
        uses: actions/checkout@v3
        
      - name: Setup Node # Installs NodeJS 18
        uses: actions/setup-node@v3
        with:
          node-version: "18"

      - name: Install dependencies # Installs your app dependencies
        run: |
          npm install

      - name: 'Login to Azure Subscription' # This is required for OIDC authentication to Azure
        uses: azure/login@v1
        with:
            client-id: ${{ secrets.AZURE_CLIENT_ID }}
            tenant-id: ${{ secrets.AZURE_TENANT_ID }}
            subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
```

### Step 3.2: Add the build NextJS app step

The next step to add is the build step. Since our app is configured for standalone output we need to move a few folders to get it running. The `public` and `static` folders are required to be in the standalone folders when the app is run, unless you choose to serve them from a CDN (Content Delivery Network). This step will move the static folder to the correct location in the standalone output. Notice the `mv public .next/standalone/public` is commented out, if you don't have anything in your public folder at build then the public folder will not be created. Only uncomment this line if you have a public folder in your output you want to include.

```yml
      - name: Build Next.js app
        run: |
          npm run build
          mv .next/static .next/standalone/.next/static
          # mv public .next/standalone/public
```

### Step 3.3 Add the deployment step

While there are a few options to actually deploy your code, I believe the following is the best way to show what is actually happening with limited abstractions.

The first thing we need to do is zip the contents of our `.next` build folder. 

***NOTE: the zip file can be whatever name you choose, I personally like to use a combination of the branch and workflow id so I can trace the path of the deployment later***

This uses the Azure App Service zipDeploy method initiated by the Azure CLI command, if you would like to read more about deployments you can reference the [Microsoft documentation for deployment best practices](https://learn.microsoft.com/en-us/azure/app-service/deploy-best-practices).

```yml
      - name: Deploy
        shell: pwsh
        run: |
            $ZIP_FILE_NAME = "./${FILE_NAME}.zip"
            zip $ZIP_FILE_NAME ./* .next -qr

            az webapp deploy `
                --resource-group $RESOURCE_GROUP_NAME `
                --name $WEB_APP_NAME `
                --src-path $ZIP_FILE_NAME `
                --slot staging `
                --type zip `
                --clean true 
        env:
            FILE_NAME: "<Name of your choice>"
            RESOURCE_GROUP_NAME: "<Name of Resource Group>"
            WEB_APP_NAME: "<Name of Web App>"
```

# Conclusion

When I first began using NextJS configuring this was a bit of a pain, the documentation for the standalone configuration was not very clear (It's a little better now). My hope is that this information will save you time as you work with NextJS on Azure. If you have questions of concerns feel free to reach out. Thanks.