Repo
Docs
使用 Docker 部署

使用 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.jsonapps/docs/package.jsonpackage-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