Docker
构建 Docker 镜像是部署各种应用的常用方法。但是,从 monorepo 执行此操作有几个挑战。
¥Building a Docker image is a common way to deploy all sorts of applications. However, doing so from a monorepo has several challenges.
Good to know:
This guide assumes you're using create-turbo or a repository with a similar structure.问题
¥The problem
在 monorepo 中,不相关的更改可能会导致 Docker 在部署应用时执行不必要的工作。
¥In a monorepo, unrelated changes can make Docker do unnecessary work when deploying your app.
假设你有一个如下所示的 monorepo:
¥Let's imagine you have a monorepo that looks like this:
你希望使用 Docker 部署 apps/api
,因此你创建了一个 Dockerfile:
¥You want to deploy apps/api
using Docker, so you create a Dockerfile:
这会将根 package.json
和根锁定文件复制到 Docker 镜像。然后,它将安装依赖,复制应用源代码并启动应用。
¥This will copy the root package.json
and the root lockfile to the Docker image. Then, it'll install dependencies, copy the app source and start the app.
你还应该创建一个 .dockerignore
文件,以防止 node_modules 与应用源代码一起复制。
¥You should also create a .dockerignore
file to prevent node_modules from being copied in with the app's source.
锁文件更改过于频繁
¥The lockfile changes too often
Docker 在部署应用方面非常智能。就像 Turborepo 一样,它会尝试执行 尽可能少的工作 的操作。
¥Docker is pretty smart about how it deploys your apps. Just like Turborepo, it tries to do as little work as possible.
在我们的 Dockerfile 中,只有当其镜像中的文件与上次运行的文件不同时,它才会运行 npm install
。如果没有,它将恢复之前的 node_modules
目录。
¥In our Dockerfile's case, it will only run npm install
if the files it has in its image are different from the previous run. If not, it'll restore the node_modules
directory it had before.
这意味着每当 package.json
、apps/api/package.json
或 package-lock.json
发生更改时,Docker 镜像都会运行 npm install
。
¥This means that whenever package.json
, apps/api/package.json
or package-lock.json
change, the Docker image will run npm install
.
听起来很棒 - 直到我们意识到一些事情。package-lock.json
对于 monorepo 来说是全局的。这意味着如果我们在 apps/web
中安装一个新包,就会导致 apps/api
重新部署。
¥This sounds great - until we realize something. The package-lock.json
is global for the monorepo. That means that if we install a new package inside apps/web
, we'll cause apps/api
to redeploy.
在大型的 monorepo 中,这可能会导致大量时间损失,因为对 monorepo 的 lockfile 的任何更改都会级联到数十或数百个部署中。
¥In a large monorepo, this can result in a huge amount of lost time, as any change to a monorepo's lockfile cascades into tens or hundreds of deploys.
解决方案
¥The solution
解决方案是将 Dockerfile 的输入精简为仅包含绝对必要的内容。Turborepo 提供了一个简单的解决方案 - turbo prune
。
¥The solution is to prune the inputs to the Dockerfile to only what is strictly necessary. Turborepo provides a simple solution - turbo prune
.
运行此命令会在 ./out
目录中创建一个精简版本的 monorepo。它仅包含 api
所依赖的工作区。它还会修剪锁定文件,以便只下载相关的 node_modules
。
¥Running this command creates a pruned version of your monorepo inside an ./out
directory. It only includes workspaces which api
depends on. It also prunes the lockfile so that only the relevant node_modules
will be downloaded.
--docker
标志
¥The --docker
flag
默认情况下,turbo prune
将所有相关文件放在 ./out
中。但为了优化 Docker 的缓存,理想情况下我们希望分两个阶段复制文件。
¥By default, turbo prune
puts all relevant files inside ./out
. But to optimize caching with Docker, we ideally want to copy the files over in two stages.
首先,我们只想复制安装包所需的内容。运行 --docker
时,你会在 ./out/json
中找到它。
¥First, we want to copy over only what we need to install the packages. When running --docker
, you'll find this inside ./out/json
.
之后,你可以复制 ./out/full
中的文件以添加源文件。
¥Afterwards, you can copy the files in ./out/full
to add the source files.
以这种方式拆分依赖和源文件,可以让我们仅在依赖发生变化时运行 npm install
。 - 带来更大的加速。
¥Splitting up dependencies and source files in this way lets us only run npm install
when dependencies change - giving us a much larger speedup.
如果没有 --docker
,所有修剪的文件都将放置在 ./out
中。
¥Without --docker
, all pruned files are placed inside ./out
.
示例
¥Example
我们详细的 with-docker
示例 深入介绍了如何充分发挥 prune
的潜力。以下是 Dockerfile,为方便起见,已复制过来。
¥Our detailed with-docker
example goes into depth on how to use prune
to its full potential. Here's the Dockerfile, copied over for convenience.
从 monorepo 的根目录构建 Dockerfile:
¥Build the Dockerfile from the root of your monorepo:
此 Dockerfile 是为使用 standalone
输出模式 的 Next.js 应用编写的。
¥This Dockerfile is written for a Next.js app that is
using the standalone
output
mode.
远程缓存
¥Remote Caching
要在 Docker 构建期间利用远程缓存,你需要确保你的构建容器具有访问 远程缓存 的凭据。
¥To take advantage of remote caches during Docker builds, you will need to make sure your build container has credentials to access your Remote Cache.
有很多方法可以保护 Docker 镜像中的密钥信息。我们将在此使用一种简单的策略,即使用 secret 作为构建参数进行多阶段构建,这些参数将在最终镜像中隐藏。
¥There are many ways to take care of secrets in a Docker image. We will use a simple strategy here with multi-stage builds using secrets as build arguments that will get hidden for the final image.
假设你使用的 Dockerfile 与上述类似,我们将在 turbo build
之前从构建参数中引入一些环境变量:
¥Assuming you are using a Dockerfile similar to the one above, we will bring in some environment variables from build arguments right before turbo build
:
turbo
现在将能够访问远程缓存。要查看非缓存 Docker 构建镜像的 Turborepo 缓存命中情况,请从项目根目录运行如下命令:
¥turbo
will now be able to hit your Remote Cache. To see a Turborepo cache hit for a non-cached Docker build image, run a command like this one from your project root: