Cloud Native Buildpacks

Creating Buildpacks

Demo Buildpack Buildplan

In this article, we demonstrate how to update your detect and build scripts to dynamically set the Node.js version specified by the user in the package.json file. The package.json file includes an "engines" section where you can declare the required Node.js version. In our example, the package.json file specifies Node.js version 18.18.0.


Updating the Detect Script

The detect script now sets a default Node.js version and then attempts to extract the user-specified version from the package.json file by reading the "engines.node" property. Finally, it writes the appropriate "provides" and "requires" fields to the buildpack build plan, which the build script uses later to download and install the correct Node.js version.

if [[ -f package.json ]]; then
  # default version
  version=18.18.1
  # Determine Node.js version from package.json using jq
  version=$(jq -r '.engines.node' "./package.json")
  # echo "version: ${version}"

  cat > "${CNB_BUILD_PLAN_PATH}" << EOL
provides = [{ name = "node-js" }]
requires = [{ name = "node-js", metadata = { version = "${version}" } }]
EOL
fi

Tip

Ensure that your package.json file contains the "engines" section with the correct Node.js version.

To test the container state, run:

docker ps

Expected output for docker ps:

CONTAINER ID   IMAGE   COMMAND               CREATED         STATUS         PORTS                    NAMES
28f480564d5e   myapp   "/cnb/process/web"   2 seconds ago   Up 2 seconds   0.0.0.0:8000->8080/tcp, [::]:8000->8080/tcp   eager

Verify the application response with:

curl localhost:8000

Output:

Hello, World!

Updating the Build Script

Previously, the build script hardcoded Node.js version 18.18.1. In the updated script, a default Node.js version (18.18.0) is defined, and the desired version is later retrieved from the build plan. This retrieved version is then used to dynamically construct the download URL for the appropriate Node.js tarball.

Below is the revised build script:

#!/usr/bin/env bash
set -euo pipefail

echo "Building image using my-js-buildpack buildpack"

default_node_js_version="18.18.0"

echo "--> Downloading and extracting NodeJS"
node_js_url=https://nodejs.org/dist/v18.18.1/node-v18.18.1-linux-x64.tar.xz
wget -q -O - "${node_js_url}" | tar -xJf - --strip-components 1

export PATH="./bin:$PATH"
pwd
ls -la

echo "--> Installing Application Dependencies"

After building, you can check the container status with:

docker ps

Expected output:

CONTAINER ID   IMAGE     COMMAND                CREATED         STATUS         PORTS                      NAMES
28f480564d5e   myapp     "/cnb/process/web"     2 seconds ago   Up 2 seconds   0.0.0.0:8000->8080/tcp, [::]:8000->8080/tcp   eager

And confirm the application response again:

curl localhost:8000

Output:

Hello, World!

Dynamically Constructing the Node.js Download URL

The build script is now updated to dynamically include the user-specified Node.js version. After retrieving the version from the build plan, the script prints the version for verification and constructs the download URL based on that version. The snippet below shows the modified section:

echo "nodejs version: ${node_js_version}"

echo "--> Downloading and extracting NodeJS"
node_js_url=https://nodejs.org/dist/v${node_js_version}/node-v${node_js_version}-linux-x64.tar.xz
wget -q -O - "${node_js_url}" | tar -xJf - --strip-components 1

export PATH="./bin:$PATH"
pwd
ls -la

echo "--> Installing Application Dependencies"
npm ci

echo "CNB_LAYERS_DIR: ${CNB_LAYERS_DIR}"
cat "${CNB_LAYERS_DIR}/launch.toml" << EOL
[[processes]]
type = "web"
EOL

Run the container check again with:

docker ps

Expected output:

CONTAINER ID   IMAGE   COMMAND                  CREATED         STATUS          PORTS                     NAMES
28f480564d5e   myapp   "/cnb/process/web"      2 seconds ago   Up 2 seconds    0.0.0.0:8000->8080/tcp   eager

Verify the endpoint:

curl localhost:8000

Output:

Hello, World!

Important

Remember to delete the old container and rebuild the image to ensure the changes take effect.


Verifying the Buildpack with package.json

Once again, note that the version requested (as printed in the logs) is 18.18.0. The updated package.json looks like this:

{
  "name": "example-application",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "description": "",
  "engines": {
    "node": "18.18.0"
  },
  "dependencies": {
    "express": "^4.21.1",
    "uuid": "^11.0.2"
  }
}

The console output during the build might resemble:

[analyzer]  Restoring data for SBOM from previous image
==> DETECTING
[detector]  my-js-buildpack 0.0.1
==> RESTORING
[builder]   Building image using my-js-buildpack buildpack
[builder]   nodejs version: 18.18.0
==> Downloading and extracting NodeJS
[builder]   workspace
Total 812
[builder]   drwxr-xr-x 1 root root  4096 Nov  4 04:42 ..

After confirming this output, delete the old container and rebuild the image. The build output will then verify that the correct Node.js version is used.

To test the dynamic version retrieval, update the package.json "engines.node" value to 18.18.1 and rebuild the image. The build logs should now indicate that Node.js version 18.18.1 is installed:

{
  "name": "example-application",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "description": "",
  "engines": {
    "node": "18.18.1"
  },
  "dependencies": {
    "express": "^4.21.1",
    "uuid": "^11.0.2"
  }
}

Console output for the new build may include:

[exporter] Reusing layer 'buildpacksio/lifecycle:process-types'
[exporter] Adding label 'io.buildpacks.lifecycle.metadata'
[exporter] Adding label 'io.buildpacks.build.metadata'
[exporter] Setting default process type 'web'
[exporter] Saving myapp...
[exporter] *** Images (5d6facb4612d):
myapp
Successfully built image myapp

And later:

[exporter] Reusing layer 'buildpacksio/lifecycle:process-types'
[exporter] Adding label 'io.buildpacks.lifecycle.metadata'
[exporter] Adding label 'io.buildpacks.build.metadata'
[exporter] Adding label 'io.buildpacks.project.metadata'
[exporter] Setting default process type 'web'
[exporter] Saving myapp...
[exporter] *** Images (56dfacb4612d):
[exporter]   myapp
Successfully built image myapp
root in ubuntu-s-4vcpu-8gb-nyc1-01 in ~/buildpack-demo took 20s

This confirms that the buildpack now successfully supports dynamic Node.js version selection based on the user's specification in package.json.

Happy building!


Additional Resources

Resource TypeUse CaseExample
BuildpackCustom build processBuildpack Documentation
DockerContainer managementDocker Documentation
Node.jsJavaScript runtimeNode.js Downloads

For further details, visit the following helpful resources:

Watch Video

Watch video content

Previous
Buildpack Buildplan