DEV Community

Tyler Smith
Tyler Smith

Posted on

Prevent NPM from installing packages outside of a Docker container

If you're developing a Node.js app in a Docker container, you may want to prevent the host machine from running npm install for a number of reasons. For example: a Linux container won't be able to run packages that include bundled binaries if they were installed from MacOS or Windows. Node packages can also run arbitrary (and potentially malicious) code during installation, so you may want to isolate your host machine.

Even still, it's easy to accidentally run npm install on the host.

Thankfully, NPM provides some ways to mitigate this. NPM has a preinstall hook that runs before the npm install command, and if it returns an error, it will prevent the install command from executing.

Using NPM's preinstall hook

We can use NPM's preinstall hook to prevent running npm install on the host by checking for an environment variable that is only present in the container.

Take a look at the following package.json file:

{
  "scripts": {
    "preinstall": "[ \"$IS_CONTAINER\" != 1 ] && echo \"Installing packages is not permitted outside of a container!\" 1>&2 && exit 1; exit 0"
  },
  "dependencies": {
    "express": "^4.17.1"
  }
}
Enter fullscreen mode Exit fullscreen mode

If the environment variable IS_CONTAINER doesn't exist or isn't set to 1, NPM will echo an error message, then exit with an error code (exit 1 means
the program exited with an error). If IS_CONTAINER is set to 1, the script will exit with 0, which means it was successful and the dependencies will install without issue.

Now we need to make sure that our Docker container has the IS_CONTAINER environment variable.

Setting an environment variable in the container

We can set a IS_CONTAINER environment variable using one of the following three methods:

1. We can set this variable in the docker-compose.yml file.

services:
  node:
    image: node:lts-alpine
    working_dir: /srv/app
    command: node index.js
    volumes:
      - .:/srv/app
    environment:
      IS_CONTAINER: 1
    ports:
      - "3000:3000"
Enter fullscreen mode Exit fullscreen mode

2. We can set this variable in a Dockerfile (if you're using one).

FROM node:lts-alpine

ENV IS_CONTAINER=1

WORKDIR /usr/src/app
COPY . .
RUN npm install
CMD ["node", "index.js"]
Enter fullscreen mode Exit fullscreen mode

3. We can set it as an environment variable from the command line.

docker-compose run -e IS_CONTAINER=1 --rm node
Enter fullscreen mode Exit fullscreen mode

With the combination of adding the preinstall script in your package.json file and setting the IS_CONTAINER environment variable using one of the methods above, your Node packages will install in your container without issue when you run npm install. Conversely: if you run npm install on your host, you'll get an error that says Installing packages is not permitted outside of a container!

Unfortunately, this method will not prevent you from installing individual packages outside of your container: it will only prevent npm install from installing the packages inside package.json. For example, if you ran npm install lodash in a console on your host machine, it would install lodash without an error. There is an NPM RFC that would change this behavior, but as of time of writing this post (November 2021), it hasn't gone through.

Prevent NPM scripts from running outside of a container

You can also prevent custom NPM scripts from running outside of a container by using NPM's pre scripts. If you had an NPM script called dev (example: npm run dev), you could create a predev script that would run before dev.

Prepending any script name with pre will cause NPM to run that script first. If you had a build script, you could have a prebuild. If you had a start script, you could have a prestart.

We can use these pre scripts to look for an IS_CONTAINER environment variable the way that we did in the preinstall script.

{
  "scripts": {
    "preinstall": "[ \"$IS_CONTAINER\" != 1 ] && echo \"Installing packages is not permitted outside of a container!\" 1>&2 && exit 1; exit 0",
    "predev": "[ \"$IS_CONTAINER\" != 1 ] && echo \"Running the development script is not permitted outside of a container!\" 1>&2 && exit 1; exit 0",
    "dev": "nodemon index.js"
  },
  "dependencies": {
    "express": "^4.17.1"
  }
}
Enter fullscreen mode Exit fullscreen mode

You can learn more about pre and post scripts in the official NPM docs.

Please let me know if this post helped you, and let me know in the comments if you know a better way of doing this!

Discussion (0)