如何在monorepo项目中使用docker-compose处理node_modules

17

我正在使用yarn工作区运行Node.js monorepo项目。文件结构如下:

workspace_root
    node_modules
    package.json
    apps
        appA
            node_modules
            package.json
        appB
            node_modules
            package.json
    libs
        libA
            dist
            node_modules
            package.json

所有应用程序都是独立的,但是它们都需要libA

我正在使用docker-compose运行所有这些应用程序。 我在这里的问题是如何正确处理所有依赖项,因为我不希望node_modules文件夹与主机同步。 在本地,当我在工作区根目录运行yarn install时,它会为所有项目安装所有依赖项,并填充不同的node_modules。 在docker-compose中,理想情况下,每个应用程序都不应意识到其他应用程序。

到目前为止,我的方法是可行的,但不是理想的,也不是非常可扩展的。

version: "3.4"

services:
  # The core is in charge of installing dependencies for ALL services. Each service must for wait the core, and then
  # just do their job, not having to handle install.
  appA:
    image: node:14-alpine
    volumes: # We must load every volumes for install
        - .:/app  # Mount the whole workspace structure
        - root_node_modules:/app/node_modules
        - appA_node_modules:/app/apps/appA/node_modules
        - appB_node_modules:/app/apps/appB/node_modules
        - libA_node_modules:/app/libs/libA/node_modules
    working_dir: /app/apps/appA
    command: [sh, -c, "yarn install && yarn run start"]

  appB:
    image: node:14-alpine
    volumes: # We must load every volumes for install
        - .:/app  # Mount the whole workspace structure
        - root_node_modules:/app/node_modules
        - appB_node_modules:/app/apps/appB/node_modules
    working_dir: /app/apps/appB
    command: [sh, -c, "/scripts/wait-for-it.sh appA:4001  -- yarn run start"]

    # And so on for all apps....
  
volumes:
    root_node_modules:
        driver: local
    appA_node_modules:
        driver: local
    appB_node_modules:
        driver: local
    libA_node_modules:
        driver: local

我看到的主要缺点是:

  • appA服务负责安装所有应用程序的依赖项。
  • 我需要为每个应用程序创建一个卷+ 一个根node_modules的卷
  • 整个项目在每个服务中都被挂载,尽管我只使用了特定的文件夹

我想避免在开发过程中进行构建,因为每次添加一个依赖项都必须这样做,这相当繁琐且会减慢速度。


您是否也想使用Docker进行开发,使用卷挂载来监视文件更改并重新加载Docker化的应用程序? - Itay Wolfish
我的应用程序通过文件监控运行在 Docker 容器内。由于源代码被挂载在容器中,所有本地的更改都会立即同步到容器中,然后应用程序会重新加载这些更改。这个过程非常顺畅,因为在容器内部进行操作是非常透明的。 - Tdy
2个回答

1

我认为在你的情况下,最好的做法是构建自己的Docker镜像,而不是使用node的镜像。所以,让我们开始编码吧。首先,你应该告诉Docker忽略node_modules文件夹。为了做到这一点,你需要为每个应用程序创建一个.dockerignore和Dockerfile文件。因此,你的结构可能看起来像这样:

workspace_root
node_modules
package.json
apps
    appA
        .dockerignore
        node_modules
        Dockerfile
        package.json
    appB
        .dockerignore
        node_modules
        Dockerfile
        package.json
libs
    libA
        .dockerignore
        dist
        node_modules
        Dockerfile
        package.json

在.dockerignore文件中,您可以重复相同的值。
node_modules/
dist/

这将使docker在构建过程中忽略这些文件夹。现在来看Dockerfile本身。为了确保您的项目在容器内正常运行,最佳实践是在容器内构建项目,而不是在容器外部构建。这样可以避免许多“在我的电脑上运行良好”的问题。话虽如此,一个Dockerfile的例子可能是这样的:

# build stage
FROM node:14-alpine AS build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# production stage
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY prod_nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

在这种情况下,我也使用了nginx,以确保用户通过适当的Web服务器访问容器。最后我也会提供prod_nginx.conf。但是这里的重点是,您可以构建该镜像并将其发送到dockerhub,然后在docker-compose.yml中使用它,而不是使用原始节点镜像。
docker-compose.yml如下:
version: "3.4"

services:
  appA:
    image: mydockeraccount/appA
    container_name: container-appA
    port: 
      - "8080:80"
    ....

现在,就像承诺的一样,是prod_nginx.conf

user                    nginx;
worker_processes        1;
error_log               /var/log/nginx/error.log warn;
pid                     /var/run/nginx.pid;
events {
    worker_connections  1024;
}

http {
    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;
    log_format          main '$remote_addr - $remote_user [$time_local] "$request" '
                             '$status $body_bytes_sent "$http_referer"'
                             '"$http_user_agent" "$http_x_forwarded_for"';
    access_log          /var/log/nginx/access.log main;
    sendfile            on;
    keepalive_timeout   65;
    server {
        listen          80;
        server_name     _ default_server;
        index           index.html;
        location / {
            root        /usr/share/nginx/html;
            index       index.html;
            try_files   $uri $uri/ /index.html;
        }
    }
}

希望能对您有所帮助。最好的问候。


你对.dockerignore文件的见解正是我所缺少的。非常感谢你! - undefined

0

在本答案底部附上我创建的示例存储库。

基本上,利用yarn工作区,我为每个包/模块创建了一个通用的dockerfile以在构建时使用。

每个docker镜像都复制整个存储库(这不是后续发布产品的好做法,您可能需要为此创建不同的流程)

因此,如果将整个存储库挂载到每个运行服务中,您可以观察库中的更改(在存储库中,我配置了nodemon,因此它也会监视lib文件)

总结一下:

  1. 即使库正在更改,也可以进行热重载,因为整个项目都挂载到每个服务的docker容器中
  2. 利用yarn工作区轻松管理包并使用方便的命令
  3. 每次更改时,应该通过docker-compose分别提高每个库的docker容器来构建它们
  4. 开发过程不是任何与生产相关的流程(例如发布docker镜像)的好做法,因为镜像中可用整个存储库
  5. 一旦将库添加为带有热重载的docker服务,它们将在每次更改时重新构建,因此无需反复执行docker-compose build
无论如何,一旦库已经确定并且更改不那么频繁,我就不会太担心重复的docker-compose build,你会发现自己需要重新构建的次数会减少(但是无论如何,我也提供了解决方案)。

Github存储库示例


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