Deploy Next.js to Azure: 3 Effective Strategies for Modern Developers

Inmeta
17 min readMay 6, 2024

Are you considering using Azure to deploy your Next.js application, or looking for ways to leverage Azure’s robust features for your existing projects?

This article will guide you through various methods to deploy your Next.js application using different Azure resources and GitHub Actions. You’ll explore deployment strategies that include direct code deployment and containerization.

By deploying on Azure, you gain access to essential features like scalability, enhanced security, and seamless integration with other Azure services. Whether you’re new to Azure or looking to optimize your application deployment, this guide provides practical insights into making the most of Azure’s capabilities to boost your application’s performance and reliability.

This article is written by Svein Are Danielsen.

Svein Are is an experienced full-stack developer with a strong focus on Azure and front-end development, in addition to .NET, React, and Next.js.

Holding a BSc in Computer Science from NTNU, he works as a consultant at Inmeta, driving innovation and modernization within his projects. Working with various customers, he often leads front-end development efforts and orchestrates seamless deployments to Azure using CI/CD pipelines.

As a tech-lead and mentor, Svein Are ensures that his team of developers stays current with the latest technologies.

Passionate about sharing knowledge and fostering growth, Svein Are looks forward to sharing more of his expertise with the community.

Prerequisites

You need to have an Azure account with an active subscription. Create an account for free. And access to create new resources. and aGitHub account, If you don’t have one, sign up for free.

Tools used:

Basic knowledge about Azure, Azure CLI, Node.js, Next.js, GitHub Actions and Docker. If you are not familiar with these technologies, it might be helpful to spend some time reading through the documentation for each one before proceeding.

Setup

Create Next.js Application

For this tutorial, I will use the latest version of Next.js, version 14.2.3, as of April 2024. However, any version starting from 12.0 will be compatible with the steps described.

To create your Next.js application, run the following command. Note that the name of your app, my-app in this example, can be different depending on your preference:

yarn create next-app my-app

When configuring your application, the choices you make regarding settings might vary. I recommend selecting TypeScript, ESLint, and the app-router to enhance your application's code quality through TypeScript's types, ESLint's linting capabilities, and the modern server-first routing approach provided by the app-router. For more information about the app-router, check out the official Next.js routing documentation.

Configure Next.js

Once the project files are created, some configuration adjustments are necessary. The main configuration file for your Next.js project might be named next.config.mjs or may differ depending on your setup; however, the content and purpose of the configuration remain consistent across different setups.

In your configuration file, you will add the output property. This setting adjusts the build output to create a standalone folder, which includes only the essential files needed for production deployment, alongside select node_modules files. You can find more details on this setup in the Next.js documentation.

Here’s how to update your next.config.mjs to include the output property:


/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
};

export default nextConfig;

As detailed in the documentation:

This will create a folder at .next/standalone which can then be deployed on its own without installing node_modules.

Additionally, a minimal server.js file is also output which can be used instead of next start. This minimal server does not copy the public or .next/static folders by default as these should ideally be handled by a CDN instead, although these folders can be copied to the standalone/public and standalone/.next/static folders manually, after which server.js file will serve these automatically.

If your project utilizes the <Image /> component, consider installing sharp to enable image optimization, further enhancing your application's performance.

Azure App Service (code)

First of I will take a look at how I can deploy our application to an Azure App Service in two different ways. First off with our code, then with Docker.

Create Resources

To deploy our Next.js application, we first need to set up the necessary Azure resources. I’ll guide you through creating these using the Azure CLI. You can learn more about each resource in detail here.

Logging into Azure CLI

First, we ensure that we are authenticated and able to execute commands related to our Azure subscription:


# Log in to Azure CLI to ensure we are authorized
az login

Creating a Resource Group

A resource group in Azure acts as a logical container for grouping related resources for an Azure solution. Here, we’ll create a resource group named rg-inmeta-learn located in Western Europe:


# Create a resource group in the West Europe region
az group create \
--name rg-inmeta-learn \
--location westeurope

Setting Up the Service Plan

The service plan defines the region, size, and features of the server farm that hosts our web app. We’re setting up a basic Linux service plan here:


# Create a Linux-based service plan in the same resource group
az appservice plan create \
--resource-group rg-inmeta-learn \
--location westeurope \
--name sp-inmeta-learn \
--is-linux \
--sku B1

Deploying the App Service

Now, we’ll create the App Service which will run our Next.js application using Node.js v18 LTS. Note the use of the --startup-file option which specifies the command that Azure should use to start the application:

# Create an App Service and set the startup file to 'node server.js'
az webapp create \
--resource-group rg-inmeta-learn \
--plan sp-inmeta-learn \
--name web-inmeta-learn \
--runtime "NODE:18-lts" \
--startup-file "node server.js"

Obtaining the Publishing Profile

Finally, we’ll fetch the publishing profile, which contains the information needed to deploy our app. This profile should be securely stored as it provides access to your app:


# Retrieve and save the publishing profile for your App Service
az webapp deployment list-publishing-profiles \
--resource-group rg-inmeta-learn \
--name web-inmeta-learn \
--xml > publishProfile.xml

With these resources configured, our Azure environment is ready. We now have a basic Service Plan (B1 SKU), and our App Service is configured to run with Node v18 LTS, using the command node server.js. For more detailed information on what this setup entails, especially if the startup command is new to you, please check the Next.js standalone output documentation.

GitHub Action

Before we dive into creating our GitHub Actions, we need to secure the publish profile obtained from Azure. Copy the content from the publishProfile.xml file and add it as a secret in your GitHub repository. Navigate to the Settings tab, expand Secrets and variables, and under Actions, create a new repository secret named PUBLISH_PROFILE with the XML content.

NB! Ensure not to check this file into your GitHub repository as it contains sensitive information that should not be publicly accessible.

While it’s recommended to use other methods for handling deployment credentials, such as federated credentials (see more on federated credentials), this approach provides a straightforward setup for this tutorial.

Next, let’s set up our workflow. Create a file named app-service-code.yml in the .github/workflows directory:

name: App Service Code

on:
push:
branches:
- main
workflow_dispatch:

env:
AZURE_WEBAPP_NAME: web-inmeta-learn

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
cache: "yarn"

- name: yarn install and build
run: |
yarn install --frozen-lockfile
yarn build

- name: Copy static files
run: |
cp -R ./public ./.next/standalone/public
cp -R ./.next/static ./.next/standalone/.next/static

- name: Upload artifact for deployment job
uses: actions/upload-artifact@v4
with:
name: app
path: ./.next/standalone

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

environment:
name: production
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}

steps:
- name: Download artifact from build job
uses: actions/download-artifact@v4
with:
name: app

- name: "Deploy to Azure Web App"
id: deploy-to-webapp
uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}
slot-name: "Production"
publish-profile: ${{ secrets.PUBLISH_PROFILE }}
package: .

This workflow file ensures that both public and static files are copied post-build. We then package everything into an artifact named app, allowing us to reuse it in subsequent stages or jobs. This modular approach, while not necessary for simpler deployments, is highly recommended for managing deployments across different environments or for more complex deployment strategies.

After initiating a push to our GitHub repository, we can observe the workflow in action, culminating in the successful deployment of our Next.js application to Azure, as depicted here:

Successfully deployed Next.js application to Azure with GitHub Actions
Successfully deployed Next.js application to Azure with GitHub Actions

Azure App Service (Docker)

In this section, we explore another method to deploy a Next.js application using Docker images in Azure App Service. This approach is particularly beneficial for those seeking to port their applications to different services within Azure or to other platforms, as Docker simplifies the migration and management of applications across diverse environments.

Benefits with Docker

Deploying Next.js applications via Docker in Azure App Service brings numerous advantages that enhance development, deployment, and operational efficiency. Here are the key benefits:

Consistency Across Environments

  • Replicability: Docker ensures your application performs consistently across all environments — from development to production — eliminating the infamous “it works on my machine” issue.
  • Predictability: Using Docker enhances the predictability of your application’s behavior across various environments, leading to more reliable deployments.

Simplified Configuration

  • Standardization: Docker standardizes your application’s configuration across different environments, simplifying management and reducing errors.
  • Isolation: Containers provide isolation, allowing multiple applications to run on the same server without interfering with each other.

Enhanced Developer Productivity

  • Speed: Containers start up significantly faster than traditional virtual machines, accelerating both development and deployment processes.
  • Tool Ecosystem: Docker’s rich ecosystem supports continuous integration, continuous deployment, and infrastructure as code, boosting productivity and operational efficiency.

Scalability and Portability

  • Scalable: Docker facilitates easy scaling of applications to handle varying loads.
  • Portable: Docker containers can run on any system that supports Docker, making it easy to transfer applications across different environments.

Optimized Resource Use

  • Efficiency: Docker utilizes the host system’s kernel, which is more resource-efficient than running full virtual machines.
  • Lower Overheads: Docker’s lower overhead allows for higher utilization of underlying hardware.

Simplified Maintenance and Updates

  • Immutable Infrastructure: Containers are immutable; updates are made by replacing a container rather than changing it, simplifying maintenance.
  • Rollback Capabilities: Quick rollbacks to previous container images are possible, enhancing system availability and reducing downtime.

Create Azure Resources

To compare deployment strategies, we’ll create another App Service within the same Service Plan using the following commands:


# Create an App Service with an nginx image as a placeholder
az webapp create \
--resource-group rg-inmeta-learn \
--plan sp-inmeta-learn \
--name web-inmeta-learn-docker \
--deployment-container-image-name nginx

# Retrieve and save the publish profile for the new App Service
az webapp deployment list-publishing-profiles \
--resource-group rg-inmeta-learn \
--name web-inmeta-learn-docker \
--xml > publishProfileDocker.xml

Initially, we use the nginx image to ensure that our new App Service is operational. This setup allows us to validate that the service is up with a default Docker image, which will later be replaced by our custom deployment workflow.

Upon visiting our newly created App Service, we should see the default nginx landing page, confirming the service is active:

Landing page for the application hosted via Docker on Azure App Service
Landing page for the nginx application hosted via Docker on Azure App Service

Next, we need to store the publish profile for this App Service in our GitHub repository under a new name, PUBLISH_PROFILE_DOCKER, following the same security measures as before.

Dockerfile & .dockerignore

To prepare for deployment, we will configure a Dockerfile and a .dockerignore. The Dockerfile is adapted from Vercel's Docker example, allowing you to easily follow along and update your setup as needed based on Vercel's latest configurations.

Dockerfile

First, create a Dockerfile in the root of your project directory, next to package.json, and paste the following code adapted from Vercel's Docker example:

FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi


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

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1

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

COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
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

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD HOSTNAME="0.0.0.0" node server.js

This multi-stage Dockerfile is beneficial as it speeds up the build and deployment process, mirroring steps used in our GitHub Actions by copying the public and static folders into the standalone folder and starting the application using node server.js instead of npm run start.

.dockerignore

To ensure an efficient Docker build process, create a .dockerignore file in the same directory as the Dockerfile to prevent unnecessary files from slowing down the build. Here’s what to include:


Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git

Docker build locally

To verify our Docker setup works as expected, build and run the application locally with these commands:


docker build . -t inmeta/nextjs
docker run -p 3000:3000 inmeta/nextjs

Now, open your web browser and go to http://localhost:3000 to see your Next.js application running.

GitHub Actions

o effectively manage our Docker deployment, we’ll implement a GitHub Actions workflow that mirrors our earlier code-based approach, featuring both build and deploy stages. First, however, we need to select a Docker repository to store our built image. While there are several options, such as Docker Hub or Azure Container Registry, we will utilize GitHub’s own container registry for this example. For details on setting up a private container registry on GitHub, refer to this guide.

Let’s create a new workflow file named app-service-docker.yml in the .github/workflows directory:

name: App Service Docker

on:
push:
branches:
- main
workflow_dispatch:

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
AZURE_WEBAPP_NAME: web-inmeta-learn-docker

jobs:
build:
runs-on: ubuntu-latest

permissions:
contents: read
packages: write

steps:
- uses: actions/checkout@v4

- name: Log in to GitHub container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push container image to registry
uses: docker/build-push-action@v4
with:
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
file: ./Dockerfile

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

environment:
name: production
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}

steps:
- name: Deploy to Azure Web App
id: deploy-to-webapp
uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}
publish-profile: ${{ secrets.PUBLISH_PROFILE_DOCKER }}
images: "${{ env.REGISTRY }}/${{ github.repository }}:${{ github.sha }}"

After configuring the workflow, push the changes to your GitHub repository and monitor the action’s progress. Upon successful completion, you should see your Next.js application running on Azure, depicted in these images:

Successfully deployed Next.js with Docker to Azure App Service via GitHub Actions
Successfully deployed Next.js with Docker to Azure App Service via GitHub Actions

You can now visit the provided URL to view your Next.js application in action:

Our Next.js application deployed with Docker on Azure App Service
Our Next.js application deployed with Docker on Azure App Service

If you encounter the default nginx page initially, ensure to perform a hard refresh of your browser to load the updated content.

Azure Static Web App

Azure Static Web App is a relatively new service designed to streamline the hosting of static and dynamic web applications. Initially aimed at static web apps, this service has evolved to support “hybrid-rendering” with Next.js, making it a versatile choice for modern web development. Let’s dive into the deployment process.

Create Azure Resources

First, we need to establish a standalone managed resource rather than reusing an existing Service Plan. We’ll set up an Azure Static Web App directly linked to our GitHub repository. Learn more about this service here.

Here are the Azure CLI commands to create the resource:


# Create Azure Static Web App resource with our GitHub repo as target
az staticwebapp create \
--name swa-inmeta-learn \
--resource-group rg-inmeta-learn \
--source https://github.com/kongebra/nextjs-to-azure \
--location westeurope \
--branch main \
--login-with-github

Executing this command will prompt a user code in the CLI and open a GitHub login page in your browser. Simply enter the provided user code to authorize Azure to interact with your GitHub account.

Once the Azure resource is set up, you can view your new Azure Static Web App in the Azure portal. Here’s how it looks right after creation:

Newly created Azure Static Web App, and it’s default landing page
Newly created Azure Static Web App, and it’s default landing page

Azure automatically creates a CI/CD workflow in your GitHub repository, which you can pull locally. This workflow includes an API token added to your GitHub secrets for deployments.

Review the GitHub Workflow

The generated workflow file, typically named azure-static-web-app-{random-name}.yml, handles the build and deployment tasks:

name: Azure Static Web Apps CI/CD

on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
workflow_dispatch:

jobs:
build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Job

environment:
name: production
url: steps.builddeploy.outputs.static_web_app_url

steps:
- uses: actions/checkout@v3
with:
submodules: true
lfs: false

- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PROUD_MEADOW_053A18103 }}
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
action: "upload"
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
app_location: "/" # App source code path
api_location: "" # Api source code path - optional
output_location: "" # Built app content directory - optional
###### End of Repository/Build Configurations ######

close_pull_request_job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
name: Close Pull Request Job

steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PROUD_MEADOW_053A18103 }}
action: "close"

Initially, you might encounter an error due to a version mismatch, as Azure might incorrectly assume an older Node.js version is in use. To resolve this, update your package.json to explicitly specify the Node.js version:

So add this to your package.json:


// package.json
{
"name": "my-app",
"version": "0.1.0",
"private": true,
...,
+ "engines": {
+ "node": ">=18.17"
+ }
}

After updating and pushing these changes to GitHub, monitor your pipeline’s execution. Once successfully deployed, your Next.js app will be live:

Successfully deployed Next.js to Azure Static Web App via GitHub Actions
Successfully deployed Next.js to Azure Static Web App via GitHub Actions

Other Resources

While we have covered some primary methods for deploying Next.js applications to Azure, numerous other Azure compute services can cater to specific deployment needs. Below is a flowchart provided by Azure that guides the decision-making process for selecting an appropriate compute service based on your project’s requirements.

Compute service flowchart for choosing compute service in Azure, made by Azure
Compute service flowchart for choosing compute service in Azure, made by Azure

The flowchart illustrates various pathways depending on your application’s needs and existing infrastructure. Although not every service listed may be directly applicable to Next.js applications, many offer valuable features that could enhance your deployment strategy. Notably, Azure Static Web Apps, which now supports hybrid rendering for Next.js, isn’t shown in this flowchart, indicating the rapid evolution of Azure services to accommodate modern web technologies.

Azure VM

Azure Virtual Machines (VMs) provide the most control over the operating environment, allowing you to manage the OS, the software, and all related configurations. This is ideal if your Next.js application requires specific settings or dependencies that

aren’t supported by higher-level PaaS solutions. Setting up a VM requires a bit more effort in terms of configuration and maintenance but offers maximum flexibility.

Azure Container Instances

Azure Container Instances (ACI) offer a lightweight option for running Docker containers on Azure without having to manage servers or clusters. This service is perfect for scenarios where you need a quick, isolated execution of a container, making it suitable for batch jobs, continuous integration workflows, and scenarios where you need to handle bursts of traffic.

Azure Kubernetes Service

Azure Kubernetes Service (AKS) simplifies deploying, managing, and scaling containerized applications using Kubernetes on Azure. AKS is ideal for applications that require complex orchestration, auto-scaling, and multi-container coordination. It integrates deeply with other Azure services, providing a robust environment for running microservices architectures.

Azure Container Apps

Azure Container Apps is a relatively new service designed to run containerized applications without the complexity of managing Kubernetes infrastructure. While not fully matured, it provides a streamlined platform for deploying modern apps, supporting features like microservices, event-driven architectures, and scalable APIs. This service is especially beneficial for developers looking to leverage container benefits with minimal overhead.

Each of these services offers unique advantages and can be chosen based on the specific needs of your deployment strategy. Consider exploring these options to find the most suitable Azure service for your application’s requirements.

Cleanup Azure resources

Make sure you clean up your Azure subscription so that you or your organization don’t get any unwanted billing! To do this, simply delete the resource group, and all resources inside it will be deleted with it.

Please note that deleting the resource group like this is not reversible


az group delete \
--resource-group rg-inmeta-learn

Conclusion

We’ve explored several robust methods to deploy Next.js applications to Azure, each offering unique benefits and suited to different needs and scenarios. Whether you choose to deploy directly using Azure App Service, containerize your app with

Docker, or leverage the capabilities of Azure Static Web Apps, Azure provides the flexibility and power necessary to support your application’s requirements.

I encourage you to experiment with these methods. Start by deploying a simple Next.js application using each technique discussed. Compare the ease of deployment, performance, and cost implications to understand which approach best fits your project’s needs.

For those looking to deepen their understanding and refine their skills further, consider exploring more advanced Azure services like Azure Kubernetes Service or delve into optimizing your CI/CD workflows for better automation and efficiency.

The journey doesn’t end here. The skills you’ve acquired are just the beginning. Use this knowledge to innovate, build more resilient and scalable applications, and continue learning. Share your experiences and outcomes with the community or write a

blog post about your deployment journey. Engaging with others not only helps solidify your own understanding but can also provide invaluable insights into diverse deployment strategies and real-world challenges.

Remember, every application is unique, and the right deployment strategy can make a significant difference in performance and user satisfaction.

Happy deploying!

Closing Thoughts

I would like to give a shout-out to my employer, Inmeta Consulting AS, for motivating me to write an article about the work I do daily for our customers.

I would also like to note that this article was researched, structured, and written by me, but polished in various degrees by ChatGTP, as I myself am not the best writer and suffer from the amazing disease dyslexia.

Thank you for taking the time to read this guide. I hope it serves as a useful resource for leveraging Azure effectively for your Next.js applications.

If you have any questions or would like to discuss the topics further, please feel free to reach out or leave a comment below.

--

--

Inmeta

True innovation lies at the crossroads between desirability, viability and feasibility. And having fun while doing it! → www.inmeta.no