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

Posted

技术标签:

【中文标题】在 Docker 容器中安装 node_modules 并与主机同步【英文标题】:Install node_modules inside Docker container and synchronize them with host 【发布时间】:2018-12-08 09:47:13 【问题描述】:

我在 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 的答案都描述了外部卷的技巧。例如,this one。

该应用程序运行良好。源代码是同步的。热重载也很好用。

我唯一的问题是主机上的node_modules 文件夹是空的。是否可以将Docker容器内的node_modules文件夹与主机同步?

我已经阅读了这些答案:

    docker-compose volume on node_modules but is empty Accessing node_modules after npm install inside Docker

不幸的是,他们并没有给我太多帮助。我不喜欢the first one,因为我不想在我的主机上运行npm install,因为可能存在跨平台问题(例如主机是Windows 或Mac,Docker 容器是Debian 8 或Ubuntu 16.04) . The second one 对我也不好,因为我想在我的 Dockerfile 中运行 npm install 而不是在 Docker 容器启动后运行它。

另外,我找到了this blog post。作者试图解决我面临的同样问题。问题是 node_modules 不会同步,因为我们只是将它们从 Docker 容器复制到主机。

我希望 Docker 容器内的 node_modules 与主机同步。请考虑我想要的:

自动安装node_modules 而不是手动安装 在 Docker 容器而不是主机中安装 node_modulesnode_modules与主机同步(如果我在Docker容器内安装了一些新包,它应该自动与主机同步,无需任何手动操作)

我需要在主机上有node_modules,因为:

可以在需要时阅读源代码 IDE 需要在本地安装node_modules,以便它可以访问devDependencies,例如eslintprettier。我不想全局安装这些devDependencies

提前致谢。

【问题讨论】:

【参考方案1】:

最适合开发

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并同步主机guest

【讨论】:

【参考方案2】:

简单、完整的解决方案

您可以使用外部命名卷技巧在容器中安装 node_modules 通过将卷的存储位置配置为指向主机的 node_modules 目录来将其与主机同步。这可以通过使用local driver 和bind mount 命名的volume 来完成,如下例所示。

无论如何,卷的数据都存储在您的主机上,例如 /var/lib/docker/volumes/,因此我们只是将其存储在您的项目中。

要在 Docker Compose 中执行此操作,只需将 node_modules 卷添加到前端服务,然后在命名卷部分中配置卷,其中“设备”是 相对路径(来自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 包。

文档:

https://docs.docker.com/storage/volumes/ https://docs.docker.com/storage/bind-mounts/ https://docs.docker.com/compose/compose-file/compose-file-v3/#driver_opts

【讨论】:

这个解决方案看起来很酷。你能解释一下卷部分吗?谢谢! @madhan 在卷中它使用本地驱动程序和绑定挂载创建了一个名为 node_modules 的命名卷,最重要的部分是“设备”是存储卷数据的位置。您可以使用上面文档下的链接阅读有关卷和绑定挂载的更多信息。 谢谢,如果device 中不存在文件夹,是否可以在运行时创建一个文件夹? 谢谢杰里米。如果我的它位于根级别 ./node_modules 并且 docker 没有自动创建它。我已经手动创建它并且一切顺利。我会想办法的。【参考方案3】:

遇到这个问题并找到接受的答案非常慢,将所有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 文件夹。 这是如何工作的,Dockerfile 中的 node_modules 文件夹在挂载卷时会被覆盖。 @DanielW。我有同样的第一印象,但它可能有效,因为 CMD 在容器启动时执行(在安装卷之后)【参考方案4】:

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

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

【讨论】:

【参考方案5】:

我不确定为什么您希望您的源代码存在于容器中宿主在开发过程中相互绑定挂载。通常,您希望您的源代码存在于容器中以进行部署,而不是开发,因为代码在您的主机上可用并绑定安装。

你的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 问题,我觉得您的问题混合了开发和部署考虑,所以也许你以错误的方式思考这个问题。

【讨论】:

【参考方案6】:

正如您提到的,将您的主机 node_modules 文件夹与您的容器 node_modules 绑定并不是一个好习惯。我经常看到为此文件夹创建内部卷的解决方案。不这样做会在构建阶段造成问题。

我在尝试为 Angular 应用程序构建 docker 开发环境时遇到了这个问题,当我在主机文件夹中编辑文件时显示 tslib 错误,导致主机的 node_modules 文件夹为空(如预期的那样)。

在这种情况下,对我有帮助的廉价解决方案是使用名为 “Remote-Containers” 的 Visual Studio Code Extension。

此扩展将允许您将 Visual Studio 代码 附加到您的容器并透明地编辑您的容器文件夹中的文件。为此,它将在您的开发容器中安装一个内部 vscode 服务器。更多信息请查看this link。

但是,请确保您的卷仍然在您的 docker-compose.yml 文件中创建。

希望对你有帮助:D!

【讨论】:

我遇到的唯一问题是控制台日志。由于容器必须从主机启动,主机上的终端是显示我的 CMD 命令输出的终端(在我的情况下为ng serve)。在 VSCode 中附加到容器时,我丢失了该输出。有什么解决办法吗?【参考方案7】:

没有人提到实际使用 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 目录的。 当然!问题是 ENTRYPOINT 部分(命令)在容器生命周期方面被称为 AFTER 卷安装在已经运行的容器上,因此对文件系统所做的任何更改都将反映到主机 - 实际上是 node_modules 目录这一点是主机的目录,而不是在docker build 期间直接在图像上构建的目录。 这种方法对我不起作用。用 yarn 解析包时,卡住了很长时间。 这行得通,在所有其他答案中,由于作者所说的所有原因,这个答案更可取。 我相信这在功能上与@lewislbr 所做的相同。 ENTRYPOINT 和 CMD 发生在同一阶段(容器构建、卷安装)并且操作相同:npm install【参考方案8】:

我知道这已经解决了,但是呢:

Dockerfile:

FROM node

# Create app directory
WORKDIR /usr/src/app

# Your other staffs

EXPOSE 3000

docker-composer.yml:

version: '3.2'
services:
    api:
        build: ./path/to/folder/with/a/dockerfile
        volumes:
            - "./volumes/app:/usr/src/app"
        command: "npm start"

volumes/app/package.json


    ... ,
    "scripts": 
        "start": "npm install && node server.js"
    ,
    "dependencies": 
        ....
    
 

运行后,node_modules 将出现在您的卷中,但其内容是在容器内生成的,因此不会出现跨平台问题。

【讨论】:

@Vladyslav Turak 你能解决我对 entrypoint.sh 的困惑吗?哪个是容器路径和复制 node_module 目录的系统路径 关于避免在每个容器运行时重新安装的任何想法? @lewislbr 在每个容器运行时运行“npm install”没有害处。它不会重新安装任何已经安装的东西,因为 node_modules 将在运行之间持续存在。【参考方案9】:

感谢Vladyslav Turak 回答entrypoint.sh,我们将node_modules 从容器复制到主机。

我实现了类似的东西,但遇到了 husky、@commitlint、tslint npm 包的问题。 我无法将任何内容推送到存储库中。 原因:我将node_modules 从 Linux 复制到 Windows。在我的情况下,example: image with diff

所以我首先使用npm installnode_modules 回到Windows 的解决方案(用于IDE 和调试)。并且 Docker 镜像会包含 Linux 版本的node_modules

【讨论】:

【参考方案10】:

首先,我要感谢David Maze 和trust512 发布他们的答案。不幸的是,他们没有帮助我解决我的问题。

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

我的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 安装到node_module 的缓存目录(/usr/src/cache)中,该目录在我们的Dockerfile 中定义。之后,entrypoint.sh 会将 node_modules 从缓存目录 (/usr/src/cache) 移动到我们的应用程序目录 (/usr/src/app)。由于这一点,整个node_modules 目录将出现在我们的主机上。

看看我上面我想要的问题:

自动安装node_modules 而不是手动安装 在 Docker 容器而不是主机中安装 node_modulesnode_modules 与主机同步(如果我在 Docker 容器内安装了一些新包,应该是 自动与主机同步,无需任何手动操作

第一件事完成:node_modules 自动安装。第二件事也完成了:node_modules 安装在 Docker 容器内(因此,不会有跨平台问题)。第三件事也完成了:安装在 Docker 容器中的node_modules 将在我们的主机上可见,它们将同步!如果我们在 Docker 容器中安装一些新的包,它将立即与我们的主机同步。

需要注意的重要一点:说真的,安装在 Docker 容器中的新包会出现在/usr/src/app/node_modules 中。由于这个目录与我们的主机同步,这个新包也会出现在我们主机的node_modules目录中。但是/usr/src/cache/node_modules 此时将具有旧版本(没有这个新包)。无论如何,这对我们来说不是问题。在下一个docker-compose up --build(需要--build)期间,Docker 将重新安装node_modules(因为package.json 已更改)并且entrypoint.sh 文件会将它们移动到我们的/usr/src/app/node_modules

您应该考虑一件更重要的事情。如果你 git pull 来自远程存储库的代码或 git checkout your-teammate-branch 在 Docker 运行时,可能会有一些新的包添加到 package.json 文件中。在这种情况下,您应该使用CTRL + C 停止Docker,然后使用docker-compose up --build 再次启动它(--build 是必需的)。如果您的容器作为守护进程运行,您应该只执行 docker-compose stop 来停止容器并使用 docker-compose up --build 再次启动它(--build 是必需的)。

如果您有任何问题,请在 cmets 中告诉我。

希望这会有所帮助。

【讨论】:

使用 rsync 代替 cp 可以节省时间。如果您之前已经复制了 node_modules,该命令几乎会立即运行。例如,在我的脚本中我运行:rsync -arv /home/app/node_modules /tmp/node_modules 这个“问题”有什么官方说法吗?我正在寻找一种干净的方法,将所有工作留给容器,并有一些从容器到主机的同步机制,以便能够从本地(主机)使用更漂亮的、git、eslint。 算了。我必须将 entrypoint.sh 转换为 unix 格式。 嗨,我试过你的解决方案,我得到了/usr/local/bin/docker-entrypoint.sh: exec: line 8: /usr/src/app/entrypoint.sh: not found 谢谢你这对我很有效!小注意,因为我还没有生成package-lock.json,所以在构建过程中会产生一个错误。无需在 Dockerfile 中使用 2 行单独的行来复制 package-lock.jsonpackage.json,而是使用带有 COPY package*.json ./ 的一行来轻松覆盖两者或其中之一。【参考方案11】:

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

    摆脱外部卷,因为您不打算实际使用它的用途 - 在停止+删除后重新生成容器,其中包含专门在容器中创建的数据。

上述可能通过稍微缩短您的撰写文件来实现:

frontend:
  build: ./app/frontend
  volumes:
    - ./app/frontend:/usr/src/app
  ports:
    - 3000:3000
  environment:
    NODE_ENV: $ENV
  command: npm start
    避免不必要时与 Dockerfile 指令重叠的卷数据。

这意味着您可能需要两个 Dockerfile - 一个用于本地开发,另一个用于部署包含所有应用程序 dist 文件的胖映像。

也就是说,考虑一个开发 Dockerfile:

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

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

【讨论】:

感谢您的回答。看来我会遇到与here 描述的相同的问题,不是吗?【参考方案12】:

这里发生了三件事:

    当您运行 docker builddocker-compose build 时,您的 Dockerfile 会构建一个包含 /usr/src/app/node_modules 目录和节点安装的新映像,但没有其他内容。特别是,您的应用不在构建的映像中。 当您docker-compose up 时,volumes: ['./app/frontend:/usr/src/app'] 指令会隐藏 /usr/src/app 中的所有内容,并将主机系统内容挂载在其上。 然后volumes: ['frontend-node-modules:/usr/src/app/node_modules'] 指令将命名卷挂载到node_modules 树的顶部,隐藏相应的主机系统目录。

如果您要启动另一个容器并将命名卷附加到它,我希望您会在那里看到 node_modules 树。对于您所描述的内容,您只是不想要命名卷:删除 volumes: 块中的第二行和 docker-compose.yml 文件末尾的 volumes: 部分。

【讨论】:

感谢您的回答。我已经删除了这两行。之后,我遇到了与here 描述的相同问题。当我运行docker-compose up 时,使用docker-compose build 安装在Docker 容器中的node_modules 被应用程序的源代码(./app/frontend:/usr/src/app) 覆盖。我在主机上没有node_modules,因此,我在容器内遇到错误(即node_modules 不存在)。 是的,没错,这就是它的工作原理:挂载的卷/主机目录隐藏了图像中的内容。 Docker 永远不会自动将内容从映像复制到主机路径卷。您可以在启动容器之前在主机目录上运行npm installyarn install,或者将您的命令更改为npm install && npm start 我明白了。问题是我不想在我的主机上运行npm install,因为可能存在跨平台问题。安装在主机上的node_modules 在 Docker 容器中并不总是正常工作。可能会有很多问题,例如不同的环境、不同的NPMNode.js 版本(例如,我们在Mac 上使用Node.js 8.11.3NPM 5.6.0 运行npm install 并使用@ 将卷挂载到Ubuntu 16.04 Docker 容器987654352@ 和 NPM 6.1.0)。该应用程序将无法正常工作。据我了解,无法实现我想要的? 例如,node-sass 是一个二进制文件,它的编译位置非常重要。如果环境不同,我们会得到一个错误。阅读更多here。

以上是关于在 Docker 容器中安装 node_modules 并与主机同步的主要内容,如果未能解决你的问题,请参考以下文章

在 docker 容器中安装 PostgreSQL

docker 在容器中安装yum等软件

如何在ubunto中安装docker

无法在 docker 容器中安装 npm?

在 docker 容器中安装猫鼬

如何在 docker 容器中安装旧版本的 r 包