Hosting Next.js apps on Azure Web Apps

Posted 24 November 2022, 02:52 | by | Perma-link

We've started using Storybook for a few of the sites we're pulling together at work, while continuing to use Azure DevOps and deploying into our Azure estates. Running a simple static app produced from a Next.js site is pretty straightforward, but it turns out that running a full Next.js application on Azure App Service has a few less than obvious hoops you need to jump through to get everything lined up and working.

The key to solving this was the "With Docker" example, which provided a decent working point to compare what we had, and what we need.

Configure Next.js Output: Standalone

The main requirement is to ensure that your next.js application is configured to output as a Standalone application. In next.config.js, ensure you've set:

module.exports = {
  output: 'standalone',
}

This will create two core folders in the .next output folder:

  • standalone - This is the self contained application, including the required node_modules packages, along with a minimal server.js file which can be used instead of next start.
  • static - This is the standard static site which needs to be deployed as well.

Finally, you also have your existing public folder.

Set up your App Service

We created a Linux based App Service Plan, in our case a B1 instance.

The App Service is configured with:

  • Stack: Node
  • Major version: Node 16
  • Minor version: Node 16 LTS
  • Startup Command: Provided by deployment

Everything else as you would normally configure it (e.g. FTP disabled, HTTP 2.0, HTTPS Only On, Minimum TLS Version 1.2, etc.).

Build and Deployment

Using Azure DevOps pipelines the following tasks will create a deployable package:

- stage: Build
  jobs:
  - job: NextBuild
    pool:
      vmImage: ubuntu-latest

    steps:
    - script: |
        yarn install --frozen-lockfile
      workingDirectory: $(Build.SourcesDirectory)
      env:
        CI: true
      displayName: "Installing packages"

    # Assumes output: 'standalone' configured on the next.config.
    - script: |
        yarn build
      workingDirectory: $(Build.SourcesDirectory)
      env:
        CI: true
        NODE_ENV: "production"
      displayName: "Building Next application"

    - task: CopyFiles@2
      inputs:
        sourceFolder: $(Build.SourcesDirectory)/.next/standalone
        contents: |
          **/*
        targetFolder: $(Build.ArtifactStagingDirectory)/site-deploy
      displayName: 'Copy standalone into the root'

    - task: CopyFiles@2
      inputs:
        sourceFolder: $(Build.SourcesDirectory)/.next/static
        contents: |
          **/*
        targetFolder: $(Build.ArtifactStagingDirectory)/site-deploy/.next/static
      displayName: 'Copy static into the .next folder'

    - task: CopyFiles@2
      inputs:
        sourceFolder: $(Build.SourcesDirectory)/public
        contents: |
          **/*
        targetFolder: $(Build.ArtifactStagingDirectory)/site-deploy/public
        flattenFolders: false
      displayName: 'Copy Public folder'

    - task: ArchiveFiles@2
      inputs:
        rootFolderOrFile: $(build.artifactStagingDirectory)/site-deploy
        includeRootFolder: false
        archiveType: "zip"
        archiveFile: $(Build.ArtifactStagingDirectory)/Nextjs-site.zip
        replaceExistingArchive: true
      displayName: "Package Next application"

    - task: PublishPipelineArtifact@1
      inputs:
        artifactName: Nextjs-site
        targetPath: $(Build.ArtifactStagingDirectory)/Nextjs-site.zip
      displayName: "Publish Next Application artifact"

Obviously you'll need to update the workingDirectory and sourceFolder paths as appropriate to your repo.

Basically, the steps are:

  1. Restore your packages
  2. Build your application
  3. Copy the contents of the .next/standalone folder into the root of a staging folder
  4. Copy the contents of the .next/static folder into the same folder within the staging folder
  5. Copy the content of the public folder into the same folder within the staging folder
  6. Create a zip archive of the staging folder, without including the root folder
  7. Publish the archive as a Pipeline Artifact

The published artifact should now contain the following structure:

+ /.next
¦ + /server
¦ ¦ + /chunks
¦ ¦ + /pages
¦ + /static
¦   + /chunks
¦   + /[hash]
+ /node_modules
+ /public
+ .env
+ package.json
+ server.js

Deployment is simply a case of deploying this package with the AzureWebApp task:

- stage: Deploy
  dependsOn: Build
  condition: and(succeeded())
  jobs:
  - deployment: NextJs
    environment: "QA"
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureWebApp@1
            inputs:
              appType: webAppLinux
              azureSubscription: azureRMServiceConnection
              appName: NextWebAppName
              package: $(Pipeline.Workspace)/Nextjs-site.zip
              deploymentMethod: "zipDeploy"
              runtimeStack: 'Node|16-lts'
              startupCommand: 'node server.js'
            displayName: "Deploy site to Azure"

The important settings to call out are:

  • appType - Needs to be set to webAppLinux so that we can use the startupCommand.
  • runtimeStack - Allows the development team to adjust the major and minor versions if necessary for their specific build.
  • startupCommand - Needs to be set to node server.js. This tells the App Service container to call the server.js file from node, starting your application ready to receive requests.

And with that, you should now have a NextJS application running on a Linux based Azure App Service.

Filed under: Azure, DevOps, Next.js