Lukáš Vinclav16.09.202230 minutes
There are plenty of ways how to deploy Next.js application. Most of them are already covered in Next.js official documentation. If you are working with AWS, there are just a few solutions that can be used. The most common solutions are EC2 or using serverless-nextjs plugin to deploy on Lambda@Edge.
11th February Update
npm i serverless -g
Considering these two most common solutions in the AWS ecosystem, there are some particular things which have to be taken into account, if you are building a maintainable codebase.
Managing servers. When working in a smaller team, there is really little or no time to spend on managing custom servers. Focus should be spend on definition of product requirements and quick iteration.
Developer experience. To activate developers, it is always awesome to have access to the most recent features immediately after their release so they can be incorporated as soon as possible. New stable middleware support in Next.js? Great, update Next.hs dependency in packages.json and start implementing some cool authentication-related features in middleware.
Deployment. Quick iterations are something crucial in a project. You want to deliver smaller chunks of code/features as soon as possible. Without good and quick pipelines this is not possible. From our experience, to be able to get something on production in less than 6 minutes (build, lint, translations download, deploy) is extremely helpful.
United tooling. To increase productivity, it is crucial to use similar if not the same tooling across multiple technologies. We are using Serverless Framework for deploying our backends and we always wanted to have the same or similar configuration options for our front end. Fewer tools to learn means more energy for business logic.
All these requirements don't fit any common solution for Next.js on AWS (EC2 or serverless-nextjs) so we were required to find a new way how to meet our requirements. From our perspective, it is okay to have everything "just" on Lambda (not Lambda@Edge). With the Lambda function, we don't have to take care of server management, we can run the most recent version of Next.js, deployment is going to be fast (invalidating CloudFront with Lambda@Edge function takes quite some time) and we will use a similar configuration as we are using for our backends on Lambda.
The output of next build
will be uploaded on the Lambda function with custom serverless server which will be compiled by using serverless-esbuild.
npm install serverless serverless-esbuild esbuild serverless-http -D
When deploying something on the Lambda function the max size of the zip file must be less than 50MB and unzipped 250MB. Most of the time just node_modules/ directory is larger than 250MB so we have to find a way how to shrink the size of the package. The solution is using nft (Node File Trace). It will take care of removing all unused files from node_modules resulting in a smaller final package. The NFT is already used in Next.js when using output: standalone in next.config.js so there is nothing to do.
// next.config.js /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, swcMinify: true, images: { // Nowhere to cache the images in Lambda (read only) unoptimized: true, // Next 12.3+, other "experimental -> images -> unoptimized" }, output: "standalone", // THIS IS IMPORTANT }; module.exports = nextConfig;
Next.js has its own server included so everything we have to do is to connect this server with the Lambda handler. For this purpose, we can use custom serverless server which will build this bridge between Lambda and Next.js application. In the example below you can see an example server.
// server.ts import { NextConfig } from "next"; import NextServer from "next/dist/server/next-server"; import serverless from "serverless-http"; // @ts-ignore import { config } from "./.next/required-server-files.json"; const nextServer = new NextServer({ hostname: "localhost", port: 3000, dir: __dirname, dev: false, conf: { ...(config as NextConfig), }, }); export const handler = serverless(nextServer.getRequestHandler(), { binary: ["*/*"], });
Configuration for Serverless Framework is more or less standard. In our case, this is good because we don't have to use something that we are not used to. The only exception is serverless-esbuild plugin which is taking care of our custom server in server.ts. This part of the configuration takes server.ts transforms it into JavaScript code (server.js) and adds serverless-http dependency into it.
# serverless.yml service: serverless-next provider: name: aws runtime: nodejs16.x region: eu-central-1 apiGateway: shouldStartNameWithService: true binaryMediaTypes: - "*/*" functions: api: handler: server.handler events: - http: ANY / - http: ANY /{proxy+} plugins: - serverless-esbuild package: patterns: - ".next" - "node_modules" - "public" - "_next" - "next.config.js" - "next-i18next.config.js" - "package.json" custom: esbuild: bundle: true minify: true exclude: "*" external: - "next"
Most of the time when you are doing serverless.yml configuration, you will just run sls deploy
from the root folder and everything will be deployed. Unfortunately, this is not how it works here, and we must create a new build folder with the proper directory structure. We must ensure that the final folder will contain our most important configuration files and then the results of next build
process.
After creating the distribution folder, we can deploy the project within the new folder. Of course, it is possible to make the whole structure manually but for the sake of simplicity below is the build script.
#!/bin/bash BUILD_FOLDER=.dist next build rm -rf $BUILD_FOLDER mv .next/standalone/ $BUILD_FOLDER/ cp -r .next/static $BUILD_FOLDER/.next rm $BUILD_FOLDER/server.js cp -r next.config.js $BUILD_FOLDER/ cp serverless.yml $BUILD_FOLDER/ cp server.ts $BUILD_FOLDER/ cp -r public $BUILD_FOLDER/ cd $BUILD_FOLDER sls deploy