Building and installing an NPM package offline

Created On:

Recently I had to build an NPM package in an environment that did not have any network access 1. Normally if you run npm install --offline the build would normally fail with the following error.

npm ERR! code ENOTCACHED

This happens because NPM’s cache is not hydrated.

However it is possible to build the package if it has a package-lock.json and if all of the dependencies specified in the lock file are placed into NPM’s cache before the build. This blog post outlines how to do that so a NPM package can be built and installed in an offline environment.

Example package

For this blog post I have setup a very basic NPM package with a single dependency on the chalk package. The package is composed of the following files.

$ ls
index.js  package-lock.json  package.json

The contents of the example package.

The index.js contains:

#!/usr/bin/env node

import chalk from 'chalk';

console.log(chalk.magenta('Hello world!'));

The package.json contains:

{
  "name": "example-package",
  "private": true,
  "version": "0.0.0",
  "main": "index.js",
  "type": "module",
  "bin": "index.js",
  "dependencies": {
    "chalk": "5.2.0"
  }
}

The package-lock.json was generated by running npm i --package-lock-only.

Building offline with Docker

Setting up an offline build environment with Docker is straight forward with the following Dockerfile.

# syntax-docker/dockerfile:1
FROM node:lts-hydrogen
WORKDIR /app
COPY package.json /app
COPY index.js /app
COPY package-lock.json /app
RUN npm i --offline

Then running docker build --network none . triggers a build without network access and returns the ENOTCACHED error.

$ docker build --network none .
 => ERROR [6/6] RUN npm i --offline
------
 > [6/6] RUN npm i --offline:
#10 0.338 npm ERR! code ENOTCACHED
#10 0.339 npm ERR! request to https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz failed: cache mode is 'only-if-cached' but no cached response is available.
#10 0.340

Hydrating the cache

To overcome the ENOTCACHED dependency the chalk-5.2.0.tgz file needs to be already placed into the cache before the build starts. Assuming chalk-5.2.0.tgz is already available then using npm cache add before npm install -offline fixes the problem. The Dockerfile can be changed to:

# syntax-docker/dockerfile:1
FROM node:lts-hydrogen
WORKDIR /tmp/

# Copy dependencies
COPY chalk-5.2.0.tgz /tmp/
RUN npm cache add chalk-5.2.0.tgz

WORKDIR /app
COPY package.json /app
COPY index.js /app
COPY package-lock.json /app
RUN npm i --offline

By calling npm cache add on every dependency specified in the package-lock.json before the build, the build can occur offline.

$ docker build --no-cache --network none .
[+] Building 1.4s (14/14) FINISHED
 => [internal] load build definition from Dockerfile                                       0.0s
 => => transferring dockerfile: 38B                                                        0.0s
 => [internal] load .dockerignore                                                          0.0s
 => => transferring context: 2B                                                            0.0s
 => [internal] load metadata for docker.io/library/node:lts-hydrogen                       0.1s
 => [1/9] FROM docker.io/library/node:lts-hydrogen@sha256:a403ff0ffe7a6a8fe90fdc70289ba39  0.0s
 => [internal] load build context                                                          0.0s
 => => transferring context: 133B                                                          0.0s
 => CACHED [2/9] WORKDIR /tmp/                                                             0.0s
 => [3/9] COPY chalk-5.2.0.tgz /tmp/                                                       0.0s
 => [4/9] RUN npm cache add chalk-5.2.0.tgz                                                0.3s
 => [5/9] WORKDIR /app                                                                     0.0s
 => [6/9] COPY package.json /app                                                           0.1s
 => [7/9] COPY index.js /app                                                               0.0s
 => [8/9] COPY package-lock.json /app                                                      0.0s
 => [9/9] RUN npm i --offline                                                              0.4s
 => exporting to image                                                                     0.2s
 => => exporting layers                                                                    0.2s
 => => writing image sha256:dcf3e70d01cabfd9a6065ac5990498865e623bce3a0c890ea8a9a5e424956  0.0s

Changing the entrypoint to /app/index.js shows the package was installed correctly.

docker run -it --entrypoint /app/index.js --rm sha256:ea603bac106fe180070b608537a2f34b7ba3ce6e31018331b06f7437f781e
Hello world!

Conclusion

It’s possible to build NPM packages in an offline environment so long as the package has a package-lock.json and all of the locked dependencies are added to the NPM cache prior to running npm install --offline. This enables building NPM packages in constrained environments using standard tooling.


  1. A nix build sandbox.↩︎