How to create a Dockerized Node application with separate node_modules for container and host
Tags: node, docker
Published: 2021-01-05 18:02:12

There's a gotcha with Docker and Node where the `node_modules` directory on the host can override the `node_modules` in the container. This typically happens in dev environments, where you want your container to receive updates to files from the host as you work on them.

The way this works:

1. You have a Node-based project you'd like to run as a Docker container
2. Your `Dockerfile` runs `npm install` when building the image
3. The `node_modules` directory (likely a subdirectory of something like `/app` in the container) gets filled with all the Node packages your `package.json` references
4. The rest of your Docker image is built
5. You ask Docker to run your image as a container, asking it to link the `/app` directory in the container to the directory of the project on the host.
6. If there's a `node_modules` directory for the project on the host, it obliterates (or replaces, depending on how you feel about this) the `/app/node_modules` directory in the container.

Symptoms of this gotcha include weird dependency version mismatches, missing packages, and exclamations such as 'I updated a dependency but the [redacted] container won't install it!'

If you want to blame something, blame Node for keeping app dependencies in the same directory as the project. There are advantages to this, but there are also disadvantages -- and we're experiencing a big one right here.

A way to fix this is to have your Docker image hold its `node_modules` in a different directory. This is easier said than done; as long as your host has a `node_modules` directory that gets synced into `/app/node_modules`, Node will always prefer it since it's proximate.

The way I've accounted for this preference is to force `/app/node_modules` to be empty, regardless of what the host's `node_modules` directory contains.

## The fix

If you're like me, you skipped all the above explanation and scrolled down to this heading. Without further ado, you can fix this issue by updating your project's files with the changes I provide below.

Required tools (for my fix; possibly my fix could be adapted to use `npm`, etc.):
* `yarn`
* Docker Compose

Assumptions:
* You hold your app code in `/app` in the container
* Your `docker-compose.yml`, `Dockerfile`, and `package.json` hold all the other things you need to run your app
* You'll use whichever Node Docker base image you want; I just include my current preference as an example.
* Your `Dockerfile` has its own bespoke `CMD` or `ENTRYPOINT`, etc.

### docker-compose-yml

```yml
version: "3.8"
services:
  web:
    container_name: my-service
    hostname: my-service
    build:
      context: .  # The Dockerfile is in the current directory
    volumes:
      - .:/app
      - /app/node_modules  # Ensure `/app/node_modules` is always empty in the container
```

The final line (`- /app/node_modules ...`) is the most important. It ensures that `/app/node_modules` is always empty in the container, so that Node ignores it.

### Dockerfile

```Dockerfile
FROM node:15.3-alpine3.12
WORKDIR /  # Important for installing node packages
COPY package.json package.json
# Install Node dependencies at `/node_modules` in the container.
RUN set -x \
    && yarn install --modules-folder=/node_modules
ENV PATH=/node_modules/.bin:$PATH
WORKDIR /app
CMD ["/app/scripts/run.sh"]
```

## Wrap up

Now when your container starts up, your app will use `/node_modules` to find dependencies.

You can also run `yarn install` and do whatever you want with your `node_modules` on the host without worrying about polluting the container's environment. The `/app/node_modules` directory in the container will remain empty.

I've tested this with a Next.js project and it worked great. I hope it works for you. Happy programming!