May 5, 2021 - 10 min read

A headshot of Tom

Tom

Consultant

How to deploy Next.js without Vercel

Share
Hero

If you’re using Next.js, Vercel is the obvious choice for quick and easy hosting. Vercel gives you:

  • Git push to deploy

  • Static pages, server-rendered pages and incremental static generation

  • Serverless function deployment

  • Content served by “global edge network” - CDN

But what do you do if your application needs to be hosted in a specific region or country? Or you aren’t using Amazon Web Services or Google Cloud Platform? Or maybe you just want more control over how the application is deployed and scaled? We were running into the same problems on a recent project we did with Cineplex, where we couldn’t use Vercel because of their existing digital ecosystem. We had to get creative to use Microsoft Azure infrastructure, while still leveraging all of the great optimizations built into the Next.js framework.

This article will take you through the steps to deploy Next.js using Microsoft Azure - but they can definitely be applied to other platforms you may be using.

If you'd like to learn more about how and why these different infrastructure components work, you can check out my blog on that here. In it, I go into how Vercel works under the hood which allowed me to replicate it.

First things first

What we'll be creating:

  • A new code repository hosted by Azure devops.

  • An Azure App Service to run the Next.js application.

  • A CI/CD pipeline to build and deploy your Next.js application.

  • A CDN profile and endpoint to cache and serve your web application lightning fast.

Make sure you have the following:

Now you’re ready to get started!


Steps

Create a project and new repository in Azure devops and push your code to that repository.

You may need to configure SSH keys if you haven’t already.

There is also a known issue when deploying next.js apps using zip deploy in Azure related to symlinks.

This can be resolved by updating the start npm script from next start to node_modules/next/dist/bin/next start --port ${PORT:-3000}.

This is to explicitly specify the location of the next binary and allow Azure to configure the port that the application will run on.

Create an Azure subscription that the new resources will be created under.

Check out the article linked for how to do this.

Create your app service in Azure

From the Azure portal (portal.azure.com), create a new resource and select “Web App”

article.nextjs-recipe-create-and-select-web-app-image1

article.nextjs-recipe.name-web-app-image2

Select the subscription you created earlier and choose a name for the new Resource Group that will act as a container for any related resources you create.

Name your web app and select Node 12 LTS as the runtime and Linux as the OS. Under App Service Plan, a new plan will be generated for you and change the resource size to Free F1 (free tier).

article.nextjs-recipe.name-web-app-image2

Click review and create and after checking all the fields, click the Create button to create your new resource group, app service plan and app service.

article.nextjs-recipe-create-web-app-image3

Create build pipeline in devops

Select your project in devops and choose Pipelines from the menu then click Create Pipeline on the next screen

article.nextjs-recipe-image4

Follow the create new pipeline wizard choosing:

Connect: Azure Repos git

Select: Choose your newly created repository with your codebase

Configure: Node.js - Build a general Node.js project with npm

article.nextjs-recipe-image5

In the YAML pipeline editor update the node version to 12.x and add tasks to archive and publish the built next.js app. Your updated YAML should look like:

# Node.js
# Build a general Node.js project with npm.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript

trigger:
- master

pool:
 vmImage: ubuntu-latest

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

- script: |
   npm install
   npm run build
 displayName: 'npm install and build'
 # NOTE: These environment variables are only required if using the contentful powered sample project
 # insert your own credentials here
 env:
   CONTENTFUL_SPACE_ID: 'your space ID here'
   CONTENTFUL_ACCESS_TOKEN: 'your access token here'
   CONTENTFUL_PREVIEW_ACCESS_TOKEN: 'your preview access token here'
   CONTENTFUL_PREVIEW_SECRET: 'your preview secret here'

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

- task: PublishBuildArtifacts@1
 inputs:
   PathtoPublish: '$(Build.ArtifactStagingDirectory)'
   ArtifactName: 'drop'
   publishLocation: 'Container'

Note: The environment variables are only required for the Contentful example or if you have your own environment variables in your pipeline. For a toy project there isn’t much danger in including keys directly in your YAML file but really any secrets (keys, passwords etc) should be injected as secret variables. Keep in mind these environment variables are only injected at build time. If the app requires runtime environment variables, those will be added to the release pipeline later on.

Then click Save and Run. This will make a commit to your code repository to add this azure-pipelines.yml file to your codebase. You can click on the job to monitor progress and make sure the build, archive, and publish are successful.

If the job fails for any reason, you can click on it to view the logs for each individual step for more information as to why it failed. If the job is failing at the npm install and build phase, make sure that the same commands complete successfully on your local machine before trying to push new code to build in the pipeline.

article.nextjs-recipe-image6

To verify the published artifact you can select the job, click the artifact, download, and unzip the file and inspect the contents. The contents of the zip should match what you’d see in your local copy of the project after running npm install and npm run build.

To be sure everything will work when the artifact is deployed, you can cd into the downloaded and unzipped folder and run npm run start and you should see your application running successfully at http://localhost:3000

article.nextjs-recipe-image7

Create new service connection

We need to create a Service Connection to give Azure devops access to our Azure resources.

From the main menu choose Project Settings at the bottom of the screen and under Pipelines choose Service Connections.

Click Create Service Connection (or New Service Connection) and under Connection Type choose Azure Resource Manager. On the next step, leave the default selection as “Service Principal (automatic)”

To configure the service connection, choose the subscription and resource group that you previously created, give the service connection a name and description if needed and click Save.

article.nextjs-recipe-image8

Create release pipeline in devops

Now we need a release pipeline to take the published artifact and deploy it to our app service.

Select Pipelines > Releases from the menu and click the New Pipeline button on the next screen.

article.nextjs-recipe-image9

Choose the Azure App Service Deployment template.

article.nextjs-recipe-image10

We need to configure our release pipeline to pull the artifact that was published by our build pipeline. Click Add an Artifact and select our newly created build pipeline from the dropdown as the Source (build pipeline).

article.nextjs-recipe-image11

Now to deploy that artifact we need to edit the Stage that was created for us. Click 1 job, 1 task to open the Edit Stage Page.

Under Azure Subscription, choose the new Service Connection that was created in the previous step. Change the App type to Web App on Linux and choose the App service you created earlier. Under startup command, we need to start running the next.js server on our App service with npm start. Click Save in the top right to save changes.

article.nextjs-recipe-image12

Next, click on the actual Deploy Azure App Service task to finish the necessary configuration. Under runtime stack select “12 LTS (NODE|12-lts)”. Click save and voila, we have a release pipeline.

Note: If your app requires any runtime environment variables (i.e. if you’re fetching data on the server side for incremental static generation), you can add the individual variables under Application & Configuration Settings, click the 3 dots beside App Settings, and add each necessary environment variable. Similar to the build pipeline variables, you may want to make these secret if they are sensitive keys/passwords. You can configure them via the Variables tab on the Edit Release Pipeline task page.

To test the deployment, click the Create Release button in the top right and the Create button on the dialogue that pops up. From the pipeline page you can click on the running stage to monitor progress and click logs to view any logging of the release in progress.

Once the release is complete, navigate to your app service URL and you should see your running next.js application.

If you see the application error screen, navigate to your new app service in the Azure portal and from the menu on the left choose Log Stream to view the logs for the application.

By default, the release is set up to be triggered manually. To change that, on the pipeline screen click the lightning bolt on the artifact box, toggle the Continuous Deployment trigger to Enabled, and set it up to be triggered by a specific branch if desired. Click save and our release should now be triggered as a result of successful builds on the master branch.

article.nextjs-recipe-image13

Create CDN profile & endpoint

At this point we have a deployed Next.js application on Azure that supports static pages, server rendered pages and incrementally generated static pages. If that’s all you need, you’re good!

Note: There is no free tier for CDN resources, check out Azure CDN pricing to estimate the costs of setting one up for your application.

If you’re looking to replicate the high-performance edge network caching that Vercel provides, there are just a couple more steps (for more information on this, check out my other blog post).

Before we create the CDN profile, we need to add Microsoft. Cdn as a Resource provider under our subscription. Navigate to your subscription in the Azure portal, choose Resource Providers in the left hand navigation. Search for cdn, click the Microsoft.Cdn provider and click Register on the top menu. This will take some time to complete.

In your newly created resource group we are going to add a new resource. Navigate to your resource group and click Add. In the search bar, search CDN and click Create.

Name your cdn resource, select your pricing tier and check Create a New CDN Endpoint Now. Name your CDN endpoint (I named it the same as the CDN profile), choose Web App as the origin type and from the Origin hostname dropdown select the hostname of your azure app service that we just deployed and click Create.

article.nextjs-recipe-image14

This operation will take a minute or two to complete, you can monitor progress by clicking the bell icon in the top right of the portal.

Once the deployment of the CDN is complete, navigate to the CDN endpoint resource and click the link to the cdn endpoint in the top right. It may take 5-10 minutes for the change to propagate so if you see an error page, wait and try again.

Now that we’ve configured the endpoint to use our app service as an origin the CDN will cache any responses for the pages that you visit. We need to also tell next.js to pull the static resources (JS/CSS/images/etc) from the CDN as well.

To do this, create a next.config.js file in the root of the project and add an assetPrefix that point to your CDN endpoint. This will tell next.js to pull any static resources from that host in the production environment.

module.exports = {
  assetPrefix: process.env.NODE_ENV === 'production'
    ? 'https://your-cdn-endpoint-here'
    : ''
}

Commit and push this change and a new build & release should be triggered automatically.

When the last build and release are complete, navigate to the CDN url in your browser and open the network tab to verify everything is now coming from the CDN and lightning fast!


A few final notes and next steps

By default, the cache-control header looks like this: s-maxage=31536000, stale-while-revalidate. This will cache the page at the CDN level for a year even if you deploy new code. There are a couple solutions to this:

  • Add a task in the release pipeline to purge the cache after deployment.

  • Use the revalidate feature in next.js which will set the cache headers for you, this also allows your server to check for changes to any underlying content and regenerate pages as needed on an ongoing basis.

Share

You might also like

Transform your digital products

Talk to us today