Building and installing an NPM package offline
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.