构建和部署 Monorepo WebSocket Web 应用程序

介绍

**提示:**本文仅关注构建和部署阶段以及我遇到的问题和解决方案。对于项目本身,请随意参考我 github 上的完整存储库。

先决条件

  • 在本地机器上安装并配置 Docker,并确保其正在运行
  • 获取渲染帐户
  • 步骤

    首先,由于我使用 pnpm 作为项目的包管理器,因此我遵循了此说明使用 Docker

    创建并构建我的 Dockerfile。

    Dockerfile

    FROM node:23-slim AS base
    ENV PNPM_HOME="/pnpm"
    ENV PATH="$PNPM_HOME:$PATH"
    
    RUN corepack enable
    RUN corepack pnpm --version
    
    RUN pnpm add -g serve
    
    FROM base AS build
    COPY . /usr/src/app
    WORKDIR /usr/src/app
    
    RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
    RUN pnpm run -r build
    RUN pnpm deploy --filter=backend --prod /prod/backend
    RUN pnpm deploy --filter=web --prod /prod/web
    
    # backend
    FROM base AS backend
    COPY --from=build /prod/backend /prod/backend
    WORKDIR /prod/backend
    EXPOSE 8000
    CMD [ "pnpm", "start" ]
    
    # web
    FROM base AS web
    COPY --from=build /prod/web /prod/web
    WORKDIR /prod/web
    EXPOSE 5173
    CMD [ "serve", "-s", "dist", "-l", "5173" ]

    在本地机器上构建 Docker 镜像并运行 Docker 容器

    后端

    docker build . --target backend --tag backend:latest

    前端

    docker build . --target web --tag web:latest

    因为WebSocket应用的客户端需要连接到服务端,而服务端需要允许WebSocket客户端跨域资源共享,所以需要允许运行在Docker容器上的服务相互通信。

    在本地机器上,创建自定义的用户定义网络“my-net”

    docker network create -d bridge my-net

    在创建的网络上运行后端容器

    docker run --network=my-net -d -p 8000:8000 backend:latest

    在创建的网络上运行 Web 容器

    docker run --network=my-net -d -p 5173:5173 web:latest

    但是,由于 Render 免费套餐的限制,除了设置用户定义网络之外,还需要其他解决方案。

    在 Render 上部署 Web 服务

    解决方案 1:使用基础设施即代码

    参考渲染蓝图 (IaC)

    **注意:**我没有选择这种方法,因为目前需要信用卡信息

    services:
      - type: web
        runtime: docker
        name: backend
        envVars:
          - key: SERVER_PORT
            value: "8000"
          - key: CLIENT_URL
            value: "http://frontend.onrender.com"
        dockerCommand: |
          docker build . --target backend --tag backend:latest && \
          docker run -d -p 8000:8000 backend:latest
    
      - type: web
        runtime: docker
        name: frontend
        envVars:
          - key: VITE_SERVER_URL
            value: "http://backend.onrender.com"
        dockerCommand: |
          docker build . --target web --tag web:latest && \
          docker run -d -p 5173:5173 web:latest

    解决方案2:将Dockerfile分为2部分,分别用于后端和前端

    由于 Render 会自动根据目标 repo 的 Dockerfile 运行 docker build,为了创建 2 个 web 服务并允许它们相互通信,请根据上述 Dockerfile 在每个项目中创建 2 个 Dockerfile:

    应用程序/后端/Dockerfile

    FROM node:23-slim AS base
    ENV PNPM_HOME="/pnpm"
    ENV PATH="$PNPM_HOME:$PATH"
    
    RUN corepack enable
    RUN corepack pnpm --version
    
    FROM base AS build
    COPY . /usr/src/app
    WORKDIR /usr/src/app
    
    RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
    RUN pnpm run -r build
    RUN pnpm deploy --filter=backend --prod /prod/backend
    
    FROM base AS backend
    COPY --from=build /prod/backend /prod/backend
    WORKDIR /prod/backend
    EXPOSE 8000
    CMD [ "pnpm", "start" ]

    应用程序/网络/Dockerfile

    FROM node:23-slim AS base
    ENV PNPM_HOME="/pnpm"
    ENV PATH="$PNPM_HOME:$PATH"
    
    RUN corepack enable
    RUN corepack pnpm --version
    
    RUN pnpm add -g serve
    
    FROM base AS build
    COPY . /usr/src/app
    WORKDIR /usr/src/app
    
    RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
    RUN pnpm run -r build
    RUN pnpm deploy --filter=web --prod /prod/web
    
    FROM base AS web
    COPY --from=build /prod/web /prod/web
    WORKDIR /prod/web
    EXPOSE 5173
    CMD [ "serve", "-s", "dist", "-l", "5173" ]

    然后按照说明部署这两个Web服务

    **笔记:**

    部署之前,这里设置环境变量,分别设置后端服务和前端服务的客户端url和服务端url,如下图

    将客户端 URL 作为环境变量设置为后端服务

    Image description

    将服务器 URL 作为环境变量设置为客户端服务

    Image description

    问题与解决方案

    1. 错误:tsconfig.json:4:5 - 错误 TS6310: 引用的项目 '/socket-react-fullstack-monorepo/apps/web/tsconfig.app.json' 可能无法禁用发射

    根本原因

    待定

    解决方案

  • https://github.com/microsoft/TypeScript/issues/49844
  • 将 ``files": [],` 添加到 apps/web/tsconfig.json

    2. 错误:ERR_PNPM_LOCKFILE_BREAKING_CHANGE 锁文件 /usr/src/app/pnpm-lock.yaml 与当前 pnpm 不兼容

    在构建 docker 文件期间`RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile`

    根本原因

    因为 Dockerfile 中的 `RUN corepack` enable 下载的是 pnpm 版本 8.15.6,但是执行 `pnpm i` 生成的 pnpm-lock.yaml 文件却是本地安装的 pnpm 包(通过 `npm install -g pnpm` )生成的,版本是 9.15.1,所以版本不匹配会报错

    Image description

    解决方案

  • https://github.com/pnpm/pnpm/issues/6609
  • 更改(降级)本地安装的 pnpm 版本,使其与 Dockerfile 中 corepack 下载的目标版本(即 8.15.6)相匹配,并使用 pnpm 的目标版本生成新的 pnpm-lock.yaml

    npm install -g pnpm@8.15.6

    安装完成后,检查 pnpm 版本,确保它显示正确的目标版本(这里是 8.15.6)

    pnpm --version

    删除本地 pnpm-lock.yaml

    rm pnpm-lock.yaml

    执行pnpm install命令,生成新的rm pnpm-lock.yaml文件

    pnpm i

    3. 错误:执行命令 pnpm run -r build 和 pnpm deploy --filter=backend --prod /prod/backend 后,/prod/backend 中没有 dist/ 文件夹

    根本原因

    package.json 中缺少“files”字段

    解决方案

  • https://github.com/pnpm/pnpm/issues/5020
  • https://pnpm.io/cli/deploy
  • https://docs.npmjs.com/cli/v10/configuring-npm/package-json#files
  • 在 apps/backend/package.json 中添加文件,然后再次运行 `pnpm deploy`

    "files": [
        "dist"
      ],

    4. 错误:找不到模块‘tslib’

  • https://docs.docker.com/reference/cli/docker/container/logs/
  • 尝试使用命令运行容器后

    docker run -d -p 8000:8000 backend:latest

    根本原因

    apps/backend/package.json 中的 tslib 包位于 `devDependencies` 中,但根据 tslib 的说法,它应该位于 `dependencies` 中

    ...
    
    "devDependencies": {
        "@types/cors": "^2.8.17",
        "@types/express": "^4.17.21",
        "@types/node": "^20.14.12",
        "nodemon": "^3.1.4",
        "tslib": "^2.6.3",
        "typescript": "^5.3.3"
      },
    
    ...

    解决方案

  • https://www.npmjs.com/package/tslib
  • 将“tslib”移至“dependencies”并运行“pnpm i”以根据 apps/backend/package.json 中的新依赖项生成 pnpm-lock.yaml

    ...
    
    "dependencies": {
        "tslib": "^2.6.3",
        "cors": "^2.8.5",
        "express": "^4.19.2",
        "socket.io": "^4.7.5",
        "ts-node": "^10.9.2",
        "zod": "^3.23.8"
      }
    
    ...

    5. 错误:ERR_PNPM_NO_SCRIPT_OR_SERVER 缺少脚本启动或文件 server.js

    尝试在 Docker 容器中使用“pnpm start”启动 Vite 应用程序时

    根本原因

    apps/web/package.json 中的脚本中没有“start”

    "scripts": {
        "dev": "vite --clearScreen false",
        "build": "tsc && vite build",
        "preview": "vite preview",
        "lint": "eslint \"src/**/*.ts\""
      },

    解决方案

  • https://www.npmjs.com/package/serve
  • https://v4.vitejs.dev/guide/static-deploy.html
  • 使用 **serve** 来提供静态文件

    在 Dockerfile 中全局安装服务

    RUN pnpm add -g serve

    更改此

    ...
    
    CMD [ "pnpm", "start" ]

    ...
    
    CMD [ "serve", "-s", "dist", "-l", "5173" ]

    再次以分离模式(`-d`)构建并运行 Docker 容器,使用 `web:latest` 镜像将主机上的端口 5173 映射到容器中的端口 5173

    docker run -d -p 5173:5173 web:latest

    资源

  • Docker
  • 使成为
  • 核心包
  • 如何调试正在运行的 Docker 容器:使用 docker container exec

  • https://docs.docker.com/reference/cli/docker/container/exec/
  • 例如,检查正在执行的docker容器中是否存在某些环境变量。

    列出所有正在运行的docker容器,并查看目标容器的容器ID

    docker ps

    使用容器ID分配目标容器的伪TTY(这里的ae980e452cbe是容器ID)

    docker exec -it ae980e452cbe /bin/sh

    列出所有环境变量

    env

    示例输出

    # env             
    NODE_VERSION=23.5.0
    
    ...
    
    PWD=/prod/backend
    PNPM_HOME=/pnpm
    #