How to containerize Next.js app with buildpacks

2 August 2023

9 min read

Share
Author

Table of Contents

Introduction

Historically, Docker has been a popular choice for containerizing applications. In the cloud native era, there is another popular player for performing app image build and getting lots of attention in devops community. It's quite easy to use cloud native buildpacks to containerize the app. There is no specific file or config required in most of the cases. Cloud native buildpacks supports all the popular coding languages and work smoothly to identify the language based on the source-code to containerize app image.

The Cloud Native Buildpacks embrace modern container standards, such as the OCI image format. They take advantage of the latest capabilities of these standards, such as cross-repository blob mounting and image layer "rebasing" on Docker API v2 registries.

-- quoted from buildpacks.io

In this blog we would go through the steps to containerize the Next.js based webapp using simple steps. For building Next.js app image with Docker build, please have a look into another blog - How to containerize Next.js app with docker build.

Initial setup

We can build a Next.js based webapp with npx create-next-app@latest. We can extend the web app with more functionalities and API's.

Based of the usecase, developer can build a standalone output for the Next.js app. Related feature in Next.js for generating standalone output can be enabled in the next.config.js.

next.config.js
module.exports = {
  output: 'standalone',
}

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

NOTE: Though standalone output feature is good to have for containerizing app with docker build, but for buildpacks, it's an optional step.

Introduce Pack CLI

The simplest way to use buildpacks is by installing Pack CLI in the dev environment. There is no need to install docker or any other dependency except Pack CLI for image build.

NOTE: We might need docker to perform a test-run for the built image with docker run.

$ pack
CLI for building apps using Cloud Native Buildpacks
 
Usage:
  pack [command]
 
Available Commands:
  build                 Generate app image from source code
  builder               Interact with builders
  buildpack             Interact with buildpacks
  extension             Interact with extensions
  config                Interact with your local pack config file
  inspect               Show information about a built app image
  stack                 Interact with stacks
  rebase                Rebase app image with latest run image
  sbom                  Interact with SBoM
  completion            Outputs completion script location
  report                Display useful information for reporting an issue
  version               Show current 'pack' version
  help                  Help about any command

Pack Build requires an image name, which will be generated from the source code. Build defaults to the current directory, but you can use --path to specify another source code directory. Build requires a builder, which can either be provided directly to build using --builder, or can be set using the set-default-builder command. To specify build time environment variables, we can pass via --env or can specify path for env-file with --env-file. All the other options can be checked with --help for pack build sub-command.

NOTE1: A builder is an image that contains all the components necessary to execute a build.

NOTE2 A buildpack is a set of executables that inspects your app source code and creates a plan to build and run your application.

💡

Detailed doc for Pack build can be found at : https://buildpacks.io/docs/app-developer-guide/build-an-app

Containerize Next.js app

Containerizing Next.js app to image is as simple as running the pack build command with certain options.

  1. --name : The name and the tag of the app image.
  2. --builder : The builder to use for image build.
  3. --buildpack : The buildpack for nodejs.

We can run below command to start the image creation process.

pack build nextapp:1.0 \
    --path /home/user1/nextapp  \
    --buildpack paketo-buildpacks/nodejs  \
    --builder paketobuildpacks/builder:full

Considerations for Next.js app

  1. By default, buildpacks look into package.json for the build script and runs the command during build. So, to ensure that the build gets the env params from buildpack for production, after the next build we can either specify the build env param or we can add 2nd build script as "build:prod": "set NODE_ENV=production & next build".

  2. In case we are using Next.js standalone output generation during build for getting the app deploy resources, we need to change the start script for app as "start": "node .next/standalone/server.js". If we are not using standalone output, then we do not need any change as start script can be as "start": "next start".

$ cat package.json
{
  "name": "nextapp",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "build:prod": "set NODE_ENV=production & next build",
    "start": "node .next/standalone/server.js",
    "lint": "next lint"
  },
  ...
}

NOTE: Above package.json considers start script for Next.js app with standalone output.

Build process

With above considerations, we can run the pack build command and track the image generation. The build process downloads the builder image and corresponding buildpacks image in first attempt. The process identifies the supporting buildpacks needed for the app based on the source-code and configs defined. Also adds environment variables, which can be overridden with CLI.

$ pack build nextapp:1.0 \
    --path /home/user1/nextapp  \
    --buildpack paketo-buildpacks/nodejs  \
    --builder paketobuildpacks/builder:full
 
full: Pulling from paketobuildpacks/builder
Digest: sha256:11068838eeb5cb3a34bb34f56de5e9a5c40c1eb31995046e3076ebde7b499c9f
Status: Image is up to date for paketobuildpacks/builder:full
full-cnb: Pulling from paketobuildpacks/run
Digest: sha256:5373ba74412664ac24f132e29b94808467d8522e5f79557d5aea72571c15e7fe
Status: Image is up to date for paketobuildpacks/run:full-cnb
===> ANALYZING
Image with name "nextapp:1.0" not found
===> DETECTING
5 of 11 buildpacks participating
paketo-buildpacks/ca-certificates 3.6.2
paketo-buildpacks/node-engine     1.6.0
paketo-buildpacks/npm-install     1.1.4
paketo-buildpacks/node-run-script 1.0.10
paketo-buildpacks/npm-start       1.0.11
===> RESTORING
Restoring metadata for "paketo-buildpacks/npm-install:npm-cache" from cache
Restoring data for "paketo-buildpacks/npm-install:npm-cache" from cache
Restoring data for SBOM from cache
===> BUILDING
 
Paketo Buildpack for CA Certificates 3.6.2
  https://github.com/paketo-buildpacks/ca-certificates
  Launch Helper: Contributing to layer
    Creating /layers/paketo-buildpacks_ca-certificates/helper/exec.d/ca-certificates-helper
Paketo Buildpack for Node Engine 1.6.0
  Resolving Node Engine version
    Candidate version sources (in priority order):
                -> ""
      <unknown> -> ""
 
    Selected Node Engine version (using ): 18.16.1
 
  Executing build process
    Installing Node Engine 18.16.1
      Completed in 3.172s
 
  Generating SBOM for /layers/paketo-buildpacks_node-engine/node
      Completed in 0s
 
  Configuring build environment
    NODE_ENV     -> "production"
    NODE_HOME    -> "/layers/paketo-buildpacks_node-engine/node"
    NODE_OPTIONS -> "--use-openssl-ca"
    NODE_VERBOSE -> "false"
 
  Configuring launch environment
    NODE_ENV     -> "production"
    NODE_HOME    -> "/layers/paketo-buildpacks_node-engine/node"
    NODE_OPTIONS -> "--use-openssl-ca"
    NODE_VERBOSE -> "false"
 
    Writing exec.d/0-optimize-memory
      Calculates available memory based on container limits at launch time.
      Made available in the MEMORY_AVAILABLE environment variable.
 
Paketo Buildpack for NPM Install 1.1.4
  Resolving installation process
    Process inputs:
      node_modules      -> "Not found"
      npm-cache         -> "Not found"
      package-lock.json -> "Found"
 
    Selected NPM build process: 'npm ci'
 
  Executing build environment install process
    Running 'npm ci --unsafe-perm --cache /layers/paketo-buildpacks_npm-install/npm-cache'
 
      added 417 packages, and audited 418 packages in 10s
 
      140 packages are looking for funding
        run `npm fund` for details
 
      found 0 vulnerabilities
      Completed in 13.114s
 
  Configuring build environment
    NODE_ENV -> "development"
    PATH     -> "$PATH:/layers/paketo-buildpacks_npm-install/build-modules/node_modules/.bin"
 
  Generating SBOM for /layers/paketo-buildpacks_npm-install/build-modules
      Completed in 2.025s
 
  Executing launch environment install process
    Running 'npm prune'
 
      up to date, audited 418 packages in 1s
 
      140 packages are looking for funding
        run `npm fund` for details
 
      found 0 vulnerabilities
      Completed in 1.617s
 
  Configuring launch environment
    NODE_PROJECT_PATH   -> "/workspace"
    NPM_CONFIG_LOGLEVEL -> "error"
    PATH                -> "$PATH:/layers/paketo-buildpacks_npm-install/launch-modules/node_modules/.bin"
 
  Generating SBOM for /layers/paketo-buildpacks_npm-install/launch-modules
      Completed in 1.875s
 
 
Paketo Buildpack for Node Run Script 1.0.10
  Executing build process
    Running 'npm run build'
 
      > nextapp@0.1.0 build
      > next build
 
      - warn You are using a non-standard "NODE_ENV" value in your environment. This creates inconsistencies in the project and is strongly advised against. Read more: https://nextjs.org/docs/messages/non-standard-node-env
      Attention: Next.js now collects completely anonymous telemetry regarding usage.
      This information is used to shape Next.js' roadmap and prioritize features.
      You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
      https://nextjs.org/telemetry
 
      - info Creating an optimized production build...
      - info Compiled successfully
      - info Linting and checking validity of types...
      - info Collecting page data...
      - info Generating static pages (0/5)
      - info Generating static pages (1/5)
      2023-08-02 07:53:46 [Env: production | about] info: About page called!
      2023-08-02 07:53:46 [Env: production | home] info: Home page called!
      2023-08-02 07:53:46 [Env: production | about] info: About page called!
      - info Generating static pages (2/5)
      - info Generating static pages (3/5)
      2023-08-02 07:53:46 [Env: production | home] info: Home page called!
      - info Generating static pages (5/5)
      - info Finalizing page optimization...
 
      Route (app)                                Size     First Load JS
        /                                      137 B          78.3 kB
        /about                                 409 B          88.7 kB
       λ /api/hello                             0 B                0 B
      + First Load JS shared by all              78.1 kB
         chunks/487-a6e261780a8e8119.js         25.7 kB
         chunks/9c58a40f-267e5b531cccdcfa.js    50.5 kB
         chunks/main-app-a1fd79d1fa4b0287.js    215 B
         chunks/webpack-7e6dae598cf9ea88.js     1.71 kB
 
      Route (pages)                              Size     First Load JS
        /404                                   183 B          75.7 kB
      + First Load JS shared by all              75.5 kB
         chunks/framework-7d3832b2720a7694.js   45 kB
         chunks/main-51a549b6bdec5f8d.js        28.6 kB
         chunks/pages/_app-e266fe44633eda69.js  194 B
         chunks/webpack-7e6dae598cf9ea88.js     1.71 kB
 
      λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
        (Static)  automatically rendered as static HTML (uses no initial props)
 
      npm notice
      npm notice New minor version of npm available! 9.5.1 -> 9.8.1
      npm notice Changelog: <https://github.com/npm/cli/releases/tag/v9.8.1>
      npm notice Run `npm install -g npm@9.8.1` to update!
      npm notice
 
    Completed in 20.079s
 
Paketo Buildpack for NPM Start 1.0.11
  Assigning launch processes:
    web (default): sh /workspace/start.sh
 
===> EXPORTING
Adding layer 'paketo-buildpacks/ca-certificates:helper'
Adding layer 'paketo-buildpacks/node-engine:node'
Adding layer 'paketo-buildpacks/npm-install:launch-modules'
Adding layer 'buildpacksio/lifecycle:launch.sbom'
Adding 1/1 app layer(s)
Adding layer 'buildpacksio/lifecycle:launcher'
Adding layer 'buildpacksio/lifecycle:config'
Adding layer 'buildpacksio/lifecycle:process-types'
Adding label 'io.buildpacks.lifecycle.metadata'
Adding label 'io.buildpacks.build.metadata'
Adding label 'io.buildpacks.project.metadata'
Setting default process type 'web'
Saving nextapp:1.0...
*** Images (7ecb7d61bb14):
      nextapp:1.0
Reusing cache layer 'paketo-buildpacks/node-engine:node'
Adding cache layer 'paketo-buildpacks/npm-install:build-modules'
Adding cache layer 'paketo-buildpacks/npm-install:npm-cache'
Adding cache layer 'buildpacksio/lifecycle:cache.sbom'
Successfully built image nextapp:1.0

Conclusion

We can verify that the image is created with docker images and can be tested with docker run

$ docker images
REPOSITORY                         TAG              IMAGE ID       CREATED        SIZE
paketobuildpacks/run               full-cnb         7877b1bacecb   4 weeks ago    687MB
nextapp                            1.0              7ecb7d61bb14   43 years ago   1.31GB
paketobuildpacks/builder           full             11bf29c66db6   43 years ago   2.11GB
 
$ docker run --rm  --name app -p 3000:3000 nextapp:1.0
Listening on port 3000 url: http://2371e8d541f6:3000
2023-08-02 07:54:27 [Env: production | hello-route] info: Request headers: {}

Hope this blog will help my friends to get the steps to build app image with buildpacks in quick and easy steps. 😊