使用全局模式的 Docker COPY 文件?

我有一个由 Yarn 管理的 monorepo,我想利用 Docker 缓存层来加速我的构建,为此我想先复制 package.jsonyarn.lock文件,运行 yarn install,然后复制其余的文件。

这是我的回购结构:

packages/one/package.json
packages/one/index.js
packages/two/package.json
packages/two/index.js
package.json
yarn.lock

这就是 Dockerfile 中有趣的部分:

COPY package.json .
COPY yarn.lock .
COPY packages/**/package.json ./
RUN yarn install --pure-lockfile
COPY . .

问题是,第三个 COPY命令没有复制任何东西,我如何才能达到预期的结果?

85424 次浏览

正如在 COPY <src> <dest>的官方 Dockerfile 参考文件中提到的

COPY 指令从 <src>复制新的文件或目录,并将它们添加到位于路径 <dest>的容器的文件系统中。

为了你的案子

每个可能包含通配符,匹配将使用 Go 的 filepath.Match规则完成。

这些是 规则它们包含这个:

“ *”匹配任何非分隔符字符序列

因此,尝试在您的模式中使用 *而不是 **

只要使用 .dockerignore过滤出不需要的文件

在您的案例中,将这个添加到您的.docker 忽略。

* . js 任何要跳过的文件

我假设您的文件位于类似 /home/package.json的位置,并希望将这些文件复制到 Docker 中的 /dest

Dockerfile 应该是这样的。 复制/主/目录

这将把所有文件复制到/home 目录,除了 .dockerignore中的 list

如果你不能从技术上枚举出 Dockerfile 中所有的子目录(即为每个子目录编写 COPY packages/one/package.json packages/one/) ,但是想要两步复制所有的文件并利用 Docker 的缓存功能,你可以尝试下面的解决方案:

  • 设计一个包装器脚本(例如,在 bash 中) ,将所需的 package.json文件复制到一个使用类似层次结构构建的单独目录(例如,.deps/) ,然后调用 docker build …
  • 调整 Dockerfile 以预先复制(并重命名)单独的目录,然后调用 yarn install --pure-lockfile..。

所有的东西放在一起,这可能会导致以下文件:

。/build.bash :

#!/bin/bash


tag="image-name:latest"


rm -f -r .deps  # optional, to be sure that there is
# no extraneous "package.json" from a previous build


find . -type d \( -path \*/.deps \) -prune -o \
-type f \( -name "package.json" \) \
-exec bash -c 'dest=".deps/$1" && \
mkdir -p -- "$(dirname "$dest")" && \
cp -av -- "$1" "$dest"' bash '{}' \;
# instead of mkdir + cp, you may also want to use
# rsync if it is available in your environment...


sudo docker build -t "$tag" .

还有

./Dockerfile :

FROM …


WORKDIR /usr/src/app


# COPY package.json .  # subsumed by the following command
COPY .deps .
# and not "COPY .deps .deps", to avoid doing an extra "mv"
COPY yarn.lock .
RUN yarn install --pure-lockfile


COPY . .
# Notice that "COPY . ." will also copy the ".deps" folder; this is
# maybe a minor issue, but it could be avoided by passing more explicit
# paths than just "." (or by adapting the Dockerfile and the script and
# putting them in the parent folder of the Yarn application itself...)

有一种基于 多阶段建造特性的解决方案:

FROM node:12.18.2-alpine3.11


WORKDIR /app
COPY ["package.json", "yarn.lock", "./"]
# Step 2: Copy whole app
COPY packages packages


# Step 3: Find and remove non-package.json files
RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf


# Step 4: Define second build stage
FROM node:12.18.2-alpine3.11


WORKDIR /app
# Step 5: Copy files from the first build stage.
COPY --from=0 /app .


RUN yarn install --frozen-lockfile


COPY . .


# To restore workspaces symlinks
RUN yarn install --frozen-lockfile


CMD yarn start

Step 5上,即使 packages目录中的任何文件已经更改,也将重用层缓存。

使用 Docker 的新 BuildKit 执行器,可以在 Docker 上下文中使用绑定挂载,然后可以根据需要从中复制任何文件。

例如,下面的代码片段将所有 package.json 文件从 Docker 上下文复制到映像的 /app/目录(下面示例中的 workdir)

不幸的是,更改挂载中的任何文件仍然会导致图层缓存丢失。这可以通过使用多阶段方法 由@mbelsky 提供来解决,但是这一次不再需要显式删除。

# syntax = docker/dockerfile:1.2
FROM ... AS packages


WORKDIR /app/
RUN --mount=type=bind,target=/docker-context \
cd /docker-context/; \
find . -name "package.json" -mindepth 0 -maxdepth 4 -exec cp --parents "{}" /app/ \;


FROM ...


WORKDIR /app/
COPY --from=packages /app/ .


指定 mindepth/maxdepth参数是为了减少要搜索的目录的数量,这可以根据用例的需要进行调整/删除。

可能有必要使用环境变量 DOCKER_BUILDKIT=1来启用 BuildKit 执行程序,因为传统的执行程序会悄悄地忽略绑定挂载。

有关 BuildKit 和绑定界限 可以在这里找到的更多信息。

根据@Joost 的建议,我创建了一个 dockerfile,它利用 BuildKit 的能力来实现以下功能:

  • 通过将 npm 的缓存目录移动到构建缓存来加快 npm install
  • 如果自上次成功构建以来在 package.json文件中没有任何更改,则跳过 npm install

伪代码:

  • 从构建上下文获取所有 package.json文件
  • 将它们与上次成功构建的 package.json文件进行比较
  • 如果发现更改,运行 npm install并缓存 package.json文件 + node_modules文件夹
  • node_modules(新鲜或缓存)复制到图像中所需的位置
# syntax = docker/dockerfile:1.2
FROM node:14-alpine AS builder


# https://github.com/opencollective/opencollective/issues/1443
RUN apk add --no-cache ncurses


# must run as root
RUN npm config set unsafe-perm true


WORKDIR /app


# get a temporary copy of the package.json files from the build context
RUN --mount=id=website-packages,type=bind,target=/tmp/builder \
cd /tmp/builder/ && \
mkdir /tmp/packages && \
chown 1000:1000 /tmp/packages && \
find ./ -name "package.json" -mindepth 0 -maxdepth 6 -exec cp --parents "{}" /tmp/packages/ \;


# check if package.json files were changed since the last successful build
RUN --mount=id=website-build-cache,type=cache,target=/tmp/builder,uid=1000 \
mkdir -p /tmp/builder/packages && \
cd /tmp/builder/packages && \
(diff -qr ./ /tmp/packages/ || (touch /tmp/builder/.rebuild && echo "Found an updated package.json"));


USER node


COPY --chown=node:node . /app


# run `npm install` if package.json files were changed, or use the cached node_modules/
RUN --mount=id=website-build-cache,type=cache,target=/tmp/builder,uid=1000 \
echo "Creating NPM cache folders" && \
mkdir -p /tmp/builder/.npm && \
mkdir -p /tmp/builder/modules && \
echo "Copying latest package.json files to NPM cache folders" && \
/bin/cp -rf /tmp/packages/* /tmp/builder/modules && \
cd /tmp/builder/modules && \
echo "Using NPM cache folders" && \
npm config set cache /tmp/builder/.npm && \
if test -f /tmp/builder/.rebuild; then (echo "Installing NPM packages" && npm install --no-fund --no-audit --no-optional --loglevel verbose); fi && \
echo "copy cached NPM packages" && \
/bin/cp -rfT /tmp/builder/modules/node_modules /app/node_modules && \
rm -rf /tmp/builder/packages && \
mkdir -p /tmp/builder/packages && \
cd /app && \
echo "Caching package.json files" && \
find ./ -name "package.json" -mindepth 0 -maxdepth 6 -exec cp --parents "{}" /tmp/builder/packages/ \; && \
(rm /tmp/builder/.rebuild 2> /dev/null || true);


注: 我只使用根文件夹的 node_modules,在我的例子中,所有来自内部文件夹的包都被提升到根文件夹