使用 Docker 部署
构建一个 Docker (opens in a new tab) 镜像是部署各种应用程序的常见方法。但是,从 Monorepo 中这样做会带来一些挑战。
问题
简而言之:在 Monorepo 中,无关的更改会导致 Docker 在部署应用程序时执行不必要的操作。
假设您有一个 Monorepo,看起来像这样
├── apps
│ ├── docs
│ │ ├── server.js
│ │ └── package.json
│ └── web
│ └── package.json
├── package.json
└── package-lock.json
您想使用 Docker 部署 apps/docs
,因此您创建了一个 Dockerfile
FROM node:16
WORKDIR /usr/src/app
# Copy root package.json and lockfile
COPY package.json ./
COPY package-lock.json ./
# Copy the docs package.json
COPY apps/docs/package.json ./apps/docs/package.json
RUN npm install
# Copy app source
COPY . .
EXPOSE 8080
CMD [ "node", "apps/docs/server.js" ]
这将把根 package.json
和根锁定文件复制到 Docker 镜像中。然后,它将安装依赖项,复制应用程序源代码并启动应用程序。
您还应该创建一个 .dockerignore
文件,以防止将 node_modules 与应用程序源代码一起复制。
node_modules
npm-debug.log
锁定文件更改过于频繁
Docker 在部署应用程序方面非常智能。就像 Turbo 一样,它试图尽可能地 减少工作量 (opens in a new tab).
在我们的 Dockerfile 的情况下,它只会运行 npm install
,如果它在镜像中拥有的文件与上次运行时不同。如果不是,它将恢复之前拥有的 node_modules
目录。
这意味着,每当 package.json
、apps/docs/package.json
或 package-lock.json
发生更改时,Docker 镜像将运行 npm install
。
这听起来很棒 - 直到我们意识到一些事情。 package-lock.json
是 Monorepo 的全局文件。这意味着,如果我们在 apps/web
中安装了一个新包,我们将导致 apps/docs
重新部署。
在大型 Monorepo 中,这会导致大量的浪费时间,因为对 Monorepo 锁定文件的任何更改都会级联到数十或数百个部署中。
解决方案
解决方案是将 Dockerfile 的输入修剪到仅包含绝对必要的内容。Turborepo 提供了一个简单的解决方案 - turbo prune
。
turbo prune docs --docker
运行此命令将在 ./out
目录中创建 Monorepo 的修剪版本。它只包含 docs
依赖的工作区。
至关重要的是,它还会修剪锁定文件,以便只下载相关的 node_modules
。
--docker 标志
默认情况下,turbo prune
会将所有相关文件放在 ./out
中。但为了优化 Docker 的缓存,我们理想情况下希望分两个阶段复制文件。
首先,我们只想复制安装包所需的文件。在运行 --docker
时,你会在 ./out/json
中找到这些文件。
out
├── json
│ ├── apps
│ │ └── docs
│ │ └── package.json
│ └── package.json
├── full
│ ├── apps
│ │ └── docs
│ │ ├── server.js
│ │ └── package.json
│ ├── package.json
│ └── turbo.json
└── package-lock.json
之后,你可以将 ./out/full
中的文件复制过来,以添加源文件。
以这种方式将 **依赖项** 和 **源文件** 分开,让我们可以 **仅在依赖项更改时运行 npm install
** - 从而获得更大的提速。
如果没有 --docker
,所有修剪后的文件都将放在 ./out
中。
示例
我们详细的 with-docker
示例 (在新标签页中打开) 深入探讨了如何充分利用 prune
。以下是 Dockerfile,为了方便起见,已复制过来。
此 Dockerfile 是为使用 standalone
输出模式 (在新标签页中打开) 的 Next.js (在新标签页中打开) 应用程序编写的。
FROM node:18-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
RUN apk update
# Set working directory
WORKDIR /app
RUN yarn global add turbo
COPY . .
RUN turbo prune web --docker
# Add lockfile and package.json's of isolated subworkspace
FROM base AS installer
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install
# Build the project
COPY --from=builder /app/out/full/ .
RUN yarn turbo run build --filter=web...
FROM base AS runner
WORKDIR /app
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
COPY --from=installer /app/apps/web/next.config.js .
COPY --from=installer /app/apps/web/package.json .
# Automatically leverage output traces to reduce image size
# https://nextjs.net.cn/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
CMD node apps/web/server.js
远程缓存
要利用 Docker 构建期间的远程缓存,你需要确保你的构建容器拥有访问你的远程缓存的凭据。
在 Docker 镜像中处理秘密有很多方法。在这里,我们将使用一个简单的策略,使用多阶段构建,将秘密作为构建参数,这些参数将在最终镜像中隐藏。
假设你使用的是类似于上面所示的 Dockerfile,我们将在 turbo build
之前引入一些来自构建参数的环境变量。
ARG TURBO_TEAM
ENV TURBO_TEAM=$TURBO_TEAM
ARG TURBO_TOKEN
ENV TURBO_TOKEN=$TURBO_TOKEN
RUN yarn turbo run build --filter=web...
turbo
现在将能够访问你的远程缓存。要查看未缓存的 Docker 构建镜像的 Turborepo 缓存命中,请从你的项目根目录运行类似于以下的命令
docker build -f apps/web/Dockerfile . --build-arg TURBO_TEAM=“your-team-name” --build-arg TURBO_TOKEN=“your-token“ --no-cache