Prevent NPM from installing packages outside of a Docker container
Tags: docker, node, npm, javascript
Published: 2021-12-01 06:32:27

---
title: Prevent NPM from installing packages outside of a Docker container
published: true
description: Learn how to prevent NPM from installing packages or running commands on your host machine.
tags: docker,node,npm,javascript
//cover_image: https://direct_url_to_image.jpg
---

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](https://harry.garrood.me/blog/malicious-code-in-purescript-npm-installer/)) 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:

```json
{
  "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"
  }
}
```

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.**
```yml
services:
  node:
    image: node:lts-alpine
    working_dir: /srv/app
    command: node index.js
    volumes:
      - .:/srv/app
    environment:
      IS_CONTAINER: 1
    ports:
      - "3000:3000"
```

**2\. We can set this variable in a Dockerfile (if you're using one).**
```Dockerfile
FROM node:lts-alpine

ENV IS_CONTAINER=1

WORKDIR /usr/src/app
COPY . .
RUN npm install
CMD ["node", "index.js"]
```

**3\. We can set it as an environment variable from the command line.**
```sh
docker-compose run -e IS_CONTAINER=1 --rm node
```

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](https://github.com/npm/rfcs/issues/325) 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.

```json
{
  "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"
  }
}
```

You can learn more about `pre` and `post` scripts in [the official NPM docs](https://docs.npmjs.com/cli/v8/using-npm/scripts#pre--post-scripts).

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!