在Docker容器内安装node_modules并与主机同步

107

我在 Docker 容器内安装 node_modules 并将其与主机同步的过程中遇到了问题。我的 Docker 版本是 18.03.1-ce, build 9ee9f40,Docker Compose 的版本是 1.21.2, build a133471

我的docker-compose.yml文件如下:

# Frontend Container.
frontend:
  build: ./app/frontend
  volumes:
    - ./app/frontend:/usr/src/app
    - frontend-node-modules:/usr/src/app/node_modules
  ports:
    - 3000:3000
  environment:
    NODE_ENV: ${ENV}
  command: npm start

# Define all the external volumes.
volumes:
  frontend-node-modules: ~

我的 Dockerfile

# Set the base image.
FROM node:10

# Create and define the working directory.
RUN mkdir /usr/src/app
WORKDIR /usr/src/app

# Install the application's dependencies.
COPY package.json ./
COPY package-lock.json ./
RUN npm install
外部卷的技巧在许多博客文章和Stack Overflow答案中都有描述。例如,这个
应用程序很好地工作。源代码已经同步。热重载也很棒。
我唯一遇到的问题是主机上的node_modules文件夹为空。是否可能将Docker容器内部的node_modules文件夹与主机同步?
我已经阅读了以下答案:
1. docker-compose volume on node_modules but is empty 2. Accessing node_modules after npm install inside Docker 不幸的是,它们并没有给我带来太大帮助。我不喜欢第一个答案,因为我不想在主机上运行npm install,因为可能会出现跨平台问题(例如,主机是Windows或Mac,而Docker容器是Debian 8或Ubuntu 16.04)。第二个答案对我来说也不好,因为我想在Dockerfile中运行npm install,而不是在Docker容器启动后运行它。
此外,我发现这篇博客文章。作者试图解决我面临的同样的问题。问题在于node_modules不会被同步,因为我们只是将它们从Docker容器复制到主机上。
我希望我的Docker容器内部的node_modules与主机同步。请注意,我希望:
- 自动安装node_modules而不是手动安装。 - 在Docker容器内安装node_modules而不是在主机上安装。 - 将node_modules与主机同步(如果我在Docker容器中安装了一些新的包,则应该自动将其与主机同步,无需任何手动操作)。
我需要在主机上拥有node_modules,因为:
- 我需要时可以阅读源代码 - IDE需要本地安装node_modules,以便可以访问devDependencies,如eslintprettier。我不想全局安装这些devDependencies
13个回答

89

首先,我想感谢David Mazetrust512发布答案。不幸的是,它们并没有帮助我解决问题。

我想发布对这个问题的答案。

我的docker-compose.yml

---
# Define Docker Compose version.
version: "3"

# Define all the containers.
services:
  # Frontend Container.
  frontend:
    build: ./app/frontend
    volumes:
      - ./app/frontend:/usr/src/app
    ports:
     - 3000:3000
    environment:
      NODE_ENV: development
    command: /usr/src/app/entrypoint.sh

我的 Dockerfile:

# Set the base image.
FROM node:10

# Create and define the node_modules's cache directory.
RUN mkdir /usr/src/cache
WORKDIR /usr/src/cache

# Install the application's dependencies into the node_modules's cache directory.
COPY package.json ./
COPY package-lock.json ./
RUN npm install

# Create and define the application's working directory.
RUN mkdir /usr/src/app
WORKDIR /usr/src/app

最后但并非最不重要的是entrypoint.sh

#!/bin/bash

cp -r /usr/src/cache/node_modules/. /usr/src/app/node_modules/
exec npm start
最棘手的部分在于将node_modules安装到我们的Dockerfile中定义的/usr/src/cache目录下的node_module缓存目录中。之后,entrypoint.sh将从缓存目录(/usr/src/cache)移动node_modules到我们的应用程序目录(/usr/src/app)。由于这一点,整个node_modules目录将出现在我们的主机上。注意:如果您从远程库中拉取代码或者当Docker正在运行时执行git checkout your-teammate-branchpackage.json文件可能会增加一些新的包。在这种情况下,您应该停止Docker并使用docker-compose up --build(需要使用--build)重新启动它。

9
使用rsync替代cp可以节省时间。如果您之前已经复制了node_modules文件夹,那么这个命令几乎立即就能运行完成。例如,在我的脚本中,我运行以下命令: rsync -arv /home/app/node_modules /tmp/node_modules - Joseph Siefers
1
算了吧,我得把 entrypoint.sh 转换成 Unix 格式。 - jcarlosweb
2
嗨,我尝试了你的解决方案,但是出现了“/usr/local/bin/docker-entrypoint.sh: exec: line 8: /usr/src/app/entrypoint.sh: not found”的错误。 - saadi
3
谢谢,这对我很有效!需要注意的是,由于我还没有生成“package-lock.json”,在构建过程中会出现错误。可以在Dockerfile中使用一行代码 COPY package*.json ./来替代将复制package-lock.jsonpackage.json分为两行的操作,因为这样可以同时涵盖两个文件或其中任意一个文件。 - m4rlo
3
“RUN mkdir /usr/src/cache” 是不必要的,因为 “WORKDIR /usr/src/cache” 会创建所需的目录。 - sebassebas1313
显示剩余6条评论

19

遇到这个问题后,发现接受的答案在每次容器运行时将所有 node_modules复制到主机非常慢,我通过在容器中安装依赖项、镜像主机卷并跳过再次安装(如果存在node_modules文件夹)来解决了这个问题:

Dockerfile:

FROM node:12-alpine

WORKDIR /usr/src/app

CMD [ -d "node_modules" ] && npm run start || npm ci && npm run start

docker-compose.yml:

version: '3.8'

services:
  service-1:
    build: ./
    volumes:
      - ./:/usr/src/app

当你需要重新安装依赖时,只需删除node_modules文件夹即可。


这是我见过的最干净的解决方案,不需要额外的入口脚本。第一次运行时,我看到在我的本地文件系统上创建了一个node_modules文件夹。 - William
3
当挂载卷时,Dockerfile中的node_modules文件夹将被覆盖,这究竟是如何运作的? - Daniel W.
2
@DanielW。我有同样的第一印象,但它可能有效,因为CMD在容器启动时执行(在挂载卷之后)。 - Speerit
请解释一下这个命令的作用:CMD [ -d "node_modules" ] - Бектур Муратов
@БектурМуратов 它检查名为“node_modules”的目录是否存在。 - lewislbr
1
我见过的最优雅的解决方案是:在CMD行中只需转义\[\]这两个特殊字符,就像这样:CMD \[ -d "node_modules" \] && npm run start || npm ci && npm run start - Bogdan Kyrychuk

13

一个简单完整的解决方案

使用外部命名卷技巧,在容器中安装node_modules,并通过配置卷的存储位置指向主机的node_modules目录来与主机同步。可以使用本地驱动程序绑定挂载来实现,如下面的示例所示。

该卷的数据已经存储在主机上,类似于/var/lib/docker/volumes/,因此我们只是将其存储在您的项目内。

要在Docker Compose中执行此操作,只需将node_modules卷添加到前端服务中,然后在命名卷部分配置卷,其中“device”是从docker-compose.yml所在位置开始的相对路径,指向本地(主机)node_modules目录。

docker-compose.yml

version: '3.9'

services:
  ui:
    # Your service options...
    volumes:
      - node_modules:/path/to/node_modules

volumes:
  node_modules:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ./local/path/to/node_modules

这个解决方案的关键是永远不要直接在主机的node_modules中进行更改,而是在容器中安装、更新或删除Node软件包。
版本控制提示:当您的Node package.json/package-lock.json文件发生更改时(无论是在拉取还是切换分支时),除了重建镜像之外,您还需要删除卷,并删除其内容。
docker volume rm example_node_modules
rm -rf local/path/to/node_modules
mkdir local/path/to/node_modules

Documentation:


这个解决方案看起来很酷。您能解释一下卷部分吗?谢谢! - Madhan
@madhan 在卷中,它使用本地驱动程序创建了一个名为 node_modules 的命名卷,并使用绑定挂载,最重要的部分是“设备”是存储卷数据的位置。您可以在上面的文档链接下阅读有关卷和绑定挂载的更多信息。 - JeremyM4n
谢谢,如果在“设备”中不存在文件夹,是否可以在运行时创建一个文件夹? - Madhan
谢谢Jeremy。如果我的文件在根目录下的./node_modules,而docker没有自动创建它,我会手动创建它,然后一切都会顺利进行。我会尝试找到一个解决方法。 - Madhan

9
这里有三件事情要处理:
1. 当你运行`docker build`或`docker-compose build`时,Dockerfile会构建一个新的镜像,其中包含`/usr/src/app/node_modules`目录和Node安装程序,但没有其他内容。特别地,你的应用程序不在构建的镜像中。
2. 当你运行`docker-compose up`时,`volumes: ['./app/frontend:/usr/src/app']`指令会隐藏`/usr/src/app`中的任何内容,并在其上挂载主机系统内容。
3. 然后,`volumes: ['frontend-node-modules:/usr/src/app/node_modules']`指令会在`node_modules`树上挂载命名卷,隐藏相应的主机系统目录。
如果你启动另一个容器并将命名卷附加到它上面,我希望你能看到`node_modules`树。对于你描述的情况,你只需删除`volumes:`块中的第二行以及`docker-compose.yml`文件末尾的`volumes:`部分即可。

感谢您的回答。我已经删除了那两行代码。之后,我遇到了与此处描述的相同问题。当我运行docker-compose up时,使用docker-compose build在Docker容器中安装的node_modules被应用程序源代码(./app/frontend:/usr/src/app)覆盖。由于主机上没有node_modules,因此我在容器内部遇到错误(即node_modules不存在)。 - Vladyslav Turak
2
是的,没错,实际上就是这样工作的:挂载的卷/主机目录会隐藏镜像中的内容。Docker 永远不会自动将东西从镜像复制到主机路径卷中。您可以在启动容器之前在主机目录上运行 npm installyarn install,或者将您的命令更改为类似 npm install && npm start 的内容。 - David Maze
7
我明白了。问题是我不想在我的主机上运行 npm install,因为可能会出现跨平台问题。在主机上安装的 node_modules 不一定能够在 Docker 容器内正常工作。可能会出现很多问题,如不同的环境、不同的 NPMNode.js 版本(例如我们在 Mac 上使用 Node.js 8.11.3NPM 5.6.0 运行 npm install,并将卷挂载到 Ubuntu 16.04 的 Docker 容器上,该容器使用的是 Node.js 10.5.0NPM 6.1.0),这样应用程序就无法正常工作了。据我所知,我想要的是不可能实现的? - Vladyslav Turak
3
例如,node-sass 是一个二进制文件,它的编译位置非常重要。如果环境不同,我们将会遇到错误。在这里阅读更多信息:https://dev59.com/mFgQ5IYBdhLWcg3w-oxw。 - Vladyslav Turak

6
没有人提到使用docker的entrypoint功能来解决问题。
以下是我的可行解决方案:
Dockerfile(多阶段构建,因此既可以用于生产环境,也可以用于本地开发):
FROM node:10.15.3 as production
WORKDIR /app

COPY package*.json ./
RUN npm install && npm install --only=dev

COPY . .

RUN npm run build

EXPOSE 3000

CMD ["npm", "start"]


FROM production as dev

COPY docker/dev-entrypoint.sh /usr/local/bin/

ENTRYPOINT ["dev-entrypoint.sh"]
CMD ["npm", "run", "watch"]

docker/dev-entrypoint.sh:

#!/bin/sh
set -e

npm install && npm install --only=dev ## Note this line, rest is copy+paste from original entrypoint

if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ]; then
  set -- node "$@"
fi

exec "$@"

docker-compose.yml:

version: "3.7"

services:
    web:
        build:
            target: dev
            context: .
        volumes:
            - .:/app:delegated
        ports:
            - "3000:3000"
        restart: always
        environment:
            NODE_ENV: dev

使用这种方法,您可以实现所需的所有3个要点,并且我个人认为这是更加清晰的方式 - 不需要移动文件。


请问您能否澄清为什么这个可以工作?具体来说,我不确定它是如何通过挂载的卷使node_modules目录对主机可用的。 - mc9
当然可以!问题在于,容器生命周期中的ENTRYPOINT部分(即命令)被称为在已经运行的容器上挂载卷之后调用,因此在其中对文件系统所做的任何更改都将反映到主机上 - 实际上,在此时node_modules目录是主机的目录,而不是直接在镜像上进行docker build期间构建的目录。 - Jan Mikeš
这种方法对我没用。使用yarn解决包时,它会卡很长时间。 - Vitor Camacho
这个方法可行,与其他答案相比,出于作者所述的所有原因,这个方法更可取。 - Rudiger
我认为这与@lewislbr所做的功能上是相同的。ENTRYPOINT和CMD在同一阶段(容器构建,卷装载)发生,并且操作相同:npm install - Chris

4
将您的主机node_modules文件夹与容器node_modules绑定在一起并不是一个好的做法,正如您所提到的那样。我经常看到创建此文件夹的内部卷解决方案。如果不这样做,在构建阶段会导致问题。
当我尝试为angular应用程序构建docker开发环境时,我遇到了这个问题,当我在我的主机文件夹中编辑文件时,会显示tslib错误,因为我的主机的node_modules文件夹为空(如预期)。
在这种情况下帮助我的廉价解决方案是使用名为“Remote-Containers”的Visual Studio Code扩展程序。
该扩展程序将允许您将您的Visual Studio Code附加到您的容器,并透明地编辑您的容器文件夹中的文件。为此,它将在您的开发容器中安装一个内部vscode服务器。有关更多信息,请查看this link
但是请确保您的卷仍然在您的docker-compose.yml文件中创建。
希望这可以帮助您:D!

我唯一的问题是控制台日志。由于容器必须从主机启动,因此主机上的终端显示我的CMD命令(在我的情况下为ng serve)的输出。当在VSCode中附加到容器时,我会失去那个输出。有什么解决办法吗? - Shy Agam

3

我的解决方法是在容器启动时安装依赖项,而不是在构建时安装。

Dockerfile:

# We're using a multi-stage build so that we can install dependencies during build-time only for production.

# dev-stage
FROM node:14-alpine AS dev-stage
WORKDIR /usr/src/app
COPY package.json ./
COPY . .
# `yarn install` will run every time we start the container. We're using yarn because it's much faster than npm when there's nothing new to install
CMD ["sh", "-c", "yarn install && yarn run start"]

# production-stage
FROM node:14-alpine AS production-stage
WORKDIR /usr/src/app
COPY package.json ./
RUN yarn install
COPY . .

.dockerignore

node_modules添加到.dockerignore中,以防止在Dockerfile运行COPY . .时复制它。我们使用卷来引入node_modules

**/node_modules

docker-compose.yml

node_app:
    container_name: node_app
    build:
        context: ./node_app
        target: dev-stage # `production-stage` for production
    volumes:
        # For development:
        #   If node_modules already exists on the host, they will be copied
        #   into the container here. Since `yarn install` runs after the
        #   container starts, this volume won't override the node_modules.
        - ./node_app:/usr/src/app
        # For production:
        #   
        - ./node_app:/usr/src/app
        - /usr/src/app/node_modules

2

最适合开发

docker-compose.yml

...
  frontend:
    build: ./app/frontend
    ports:
      - 3000:3000
    volumes:
      - ./app/frontend:/usr/src/app
...

./app/frontend/Dockerfile

FROM node:lts

WORKDIR /usr/src/app

RUN npm install -g react-scripts

RUN chown -Rh node:node /usr/src/app

USER node

EXPOSE 3000

CMD [ "sh", "-c", "npm install && npm run start" ]

#FOR PROD
# CMD [ "sh", "-c", "npm install && npm run build" ]

用户node将帮助您处理主机<->客户端之间的权限问题。
文件夹node_modules将可从主机访问,并同步主机<->客户端之间的文件。

2

我不建议重叠卷,尽管我没有看到任何官方文档禁止它,但过去我遇到过一些问题。我的做法是:

  1. 摆脱外部卷,因为你并不打算按照其预定用法使用它 - 在停止+删除容器后,使用在容器内创建的数据重新生成容器。

通过缩短compose文件的长度,可以实现以上操作:

frontend:
  build: ./app/frontend
  volumes:
    - ./app/frontend:/usr/src/app
  ports:
    - 3000:3000
  environment:
    NODE_ENV: ${ENV}
  command: npm start
  1. 尽可能避免将卷数据与Dockerfile指令重叠,除非有必要。

这意味着您可能需要两个Dockerfiles - 一个用于本地开发,另一个用于部署所有应用程序dist文件层内的大型映像。

话虽如此,请考虑使用开发Dockerfile:

FROM node:10
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
RUN npm install

上面的内容让应用程序创建一个完整的node_modules安装,并将其映射到您的主机位置,而docker-compose指定的命令会启动您的应用程序。

谢谢您的回答。看起来我会遇到与我在这里描述的相同的问题,对吗? - Vladyslav Turak

2
我不确定你为什么想让源代码在容器内、主机上和开发过程中相互绑定。通常,你希望将源代码放在容器中进行部署,而不是在开发过程中,因为代码已经在主机上并且已经绑定了。请参考你的docker-compose.yml文件。
frontend:
  volumes:
    - ./app/frontend:/usr/src/app

你的 Dockerfile
FROM node:10

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

当然,您必须首次和每次package.json更改时运行npm install,但是您在容器内运行它,因此不存在跨平台问题:docker-compose exec frontend npm install

最后启动您的服务器docker-compose exec frontend npm start

然后通常在部署的CI管道中,您使用整个源代码副本构建最终镜像,并重新安装node_modules,但是此时您不再需要绑定挂载和“同步”,因此您的设置可能如下所示:

docker-compose.yml

frontend:
  build:
    context: ./app/frontend
    target: dev
  volumes:
    - ./app/frontend:/usr/src/app

Dockerfile

FROM node:10 as dev

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

FROM dev as build

COPY package.json package-lock.json ./
RUN npm install

COPY . ./

CMD ["npm", "start"]

您可以稍后定位到Dockerfile的构建阶段,手动或在管道中进行构建,以构建准备部署的镜像。

我知道这不是您问题的确切答案,因为您必须运行npm install,并且在开发过程中没有任何内容存在于容器中,但它解决了您的node_modules问题,而且我觉得您的问题混淆了开发和部署考虑因素,所以也许您以错误的方式思考了这个问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接