在Dockerfile构建期间,如何将主机卷装入docker容器

自2014年提出这个问题以来,发生了许多情况,许多事情发生了变化。今天我再次讨论这个话题,这是我第12次编辑这个问题,以反映最新的变化。这个问题可能看起来很长,但它是按倒序排列的,所以最新的变化在顶部,你可以随时停止阅读。

我想解决的问题是——如何在构建期间将主机卷挂载到Dockerfile中的docker容器中,即在docker build期间具有docker run -v /export:/export能力。

对我来说,它背后的一个原因是,当在Docker中构建东西时,我不希望那些(apt-get install)缓存锁定在单个Docker中,而是共享/重用它们。

这就是我问这个问题的主要原因。我今天面临的另一个原因是试图利用来自主机的巨大私人回购,否则我必须使用我的私人ssh密钥从docker内部的私人回购执行git clone,我不知道如何,也没有研究过。

最新更新:

@BMitch回答中的Buildkit

使用RUN --mount语法,你还可以从build-context…

它现在已经内置在docker(我认为是第三方工具),只要你的'超过18.09。我的现在是20.10.7 https://docs.docker.com/develop/develop-images/build_enhancements/ < / p >

要启用BuildKit构建

重新安装docker最简单的方法是在调用docker build命令时设置DOCKER_BUILDKIT=1环境变量,例如:

$ DOCKER_BUILDKIT=1 docker build .

否则,你会得到:

the --mount option requires BuildKit. Refer to https://docs.docker.com/go/buildkit/ to learn how to build images with BuildKit enabled

因此,它将是我上面解释的第二个用例的完美解决方案。

截至2019年5月7日更新:

在docker v18.09之前,正确的答案应该是以:

有一种方法可以在构建期间挂载卷,但它不涉及Dockerfiles。

然而,这是一个陈述不当、组织不当和支持不足的回答。当我重新安装我的docker contains时,我碰巧发现了以下文章:

< p > Dockerize apt-cache -ng服务 < br / > https://docs.docker.com/engine/examples/apt-cacher-ng/ < / p >

这是码头工人对这个/我的问题的解决方案,不是直接的,而是间接的。这是docker建议我们做的正统方法。我承认这比我在这里想问的要好。

另一种方法是新接受的答案,例如,v18.09中的Buildkit。

挑一个适合你的。


是:曾经有一个解决方案——rocker,它不是来自Docker,但现在rocker停止了,我再次将答案恢复到“不是possible"


旧的更新:所以答案是“不可能”。我可以接受这个答案,因为我知道这个问题在https://github.com/docker/docker/issues/3156上已经被广泛讨论过了。我能理解可移植性对于docker开发者来说是最重要的问题;但是作为一个docker用户,我不得不说我对这个缺失的特性感到非常失望。让我引用前面讨论中的一句话来结束我的论点:是的,我可以使用wget或curl下载我需要的任何东西,但事实上,仅仅是出于可移植性的考虑,现在迫使我下载>每次我构建一个Gentoo基映像时,1GB的Portage树既不高效也不友好。此外,包存储库总是在/usr/portage下,因此总是可以在Gentoo下移植。我再次尊重这个决定,但同时也请允许我表达我的失望。谢谢。


最初的问题详细信息

< p > 通过卷共享目录 < br / > http://docker.readthedocs.org/en/v0.7.3/use/working_with_volumes/ < / p >

它说数据卷功能“从Docker远程api的版本1开始就可以使用”。我的docker是1.2.0版本,但我发现上面文章中给出的例子不起作用:

# BUILD-USING:        docker build -t data .
# RUN-USING:          docker run -name DATA data
FROM          busybox
VOLUME        ["/var/volume1", "/var/volume2"]
CMD           ["/usr/bin/true"]

在Dockerfile中,通过VOLUME命令将主机挂载的卷装入docker容器的正确方法是什么?

$ apt-cache policy lxc-docker
lxc-docker:
Installed: 1.2.0
Candidate: 1.2.0
Version table:
*** 1.2.0 0
500 https://get.docker.io/ubuntu/ docker/main amd64 Packages
100 /var/lib/dpkg/status


$ cat Dockerfile
FROM          debian:sid


VOLUME        ["/export"]
RUN ls -l /export
CMD ls -l /export


$ docker build -t data .
Sending build context to Docker daemon  2.56 kB
Sending build context to Docker daemon
Step 0 : FROM          debian:sid
---> 77e97a48ce6a
Step 1 : VOLUME        ["/export"]
---> Using cache
---> 59b69b65a074
Step 2 : RUN ls -l /export
---> Running in df43c78d74be
total 0
---> 9d29a6eb263f
Removing intermediate container df43c78d74be
Step 3 : CMD ls -l /export
---> Running in 8e4916d3e390
---> d6e7e1c52551
Removing intermediate container 8e4916d3e390
Successfully built d6e7e1c52551


$ docker run data
total 0


$ ls -l /export | wc
20     162    1131


$ docker -v
Docker version 1.2.0, build fa7b24f
292445 次浏览

不可能使用VOLUME指令告诉docker 什么挂载。这将严重破坏可移植性。这条指令告诉docker,这些目录中的内容不会放在图像中,并且可以使用--volumes-from命令行参数从其他容器中访问。你必须使用-v /path/on/host:/path/in/container运行容器来访问主机上的目录。

无法在构建期间挂载主机卷。没有特权构建,安装主机也会严重降低可移植性。您可能想尝试使用wget或curl下载构建所需的任何内容,并将其放置到合适的位置。

在运行容器时,将在主机上创建一个目录并将其挂载到容器中。你可以找到它所在的目录

$ docker inspect --format "\{\{ .Volumes }}" <ID>
map[/export:/var/lib/docker/vfs/dir/<VOLUME ID...>]

如果你想在容器中挂载主机上的目录,你必须使用-v参数并指定目录。在你的情况下,这将是:

docker run -v /export:/export data

你可以在容器中使用hosts文件夹。

我认为你可以通过docker命令来运行构建,这个命令本身是在docker容器中运行的。看到Docker现在可以在Docker | Docker Blog中运行。类似这样的技术,但实际上是从容器中访问外部码头器,被使用,例如,当探索如何创建最小的Docker容器| Xebia Blog. xml时。

另一篇相关文章是优化Docker图像| CenturyLink实验室,它解释了如果你在构建过程中下载了东西,你可以通过在一个RUN步骤中下载、构建和删除下载来避免在最终映像中浪费空间。

有一种方法可以在构建期间挂载卷,但它不涉及Dockerfiles。

该技术将从你想使用的任何基础上创建容器(使用-v选项将你的卷安装到容器中),运行shell脚本来进行映像构建工作,然后在完成后提交容器作为映像。

这不仅省去了您不想要的多余文件(这也适用于安全文件,如SSH文件),而且还创建了单个映像。它也有缺点:commit命令不支持所有的Dockerfile指令,如果你需要编辑构建脚本,它不允许你在你离开的时候重新开始。

更新:

例如,

CONTAINER_ID=$(docker run -dit ubuntu:16.04)
docker cp build.sh $CONTAINER_ID:/build.sh
docker exec -t $CONTAINER_ID /bin/sh -c '/bin/sh /build.sh'
docker commit $CONTAINER_ID $REPO:$TAG
docker stop $CONTAINER_ID

更新:有人就是不会把“不”作为答案,我很喜欢,尤其是这个特别的问题。

好消息,现在有办法了

解决方案是Rocker: https://github.com/grammarly/rocker

约翰亚尼“在我看来,它解决了Dockerfile的所有弱点,使其适合开发。”< / em >

摇滚歌手

https://github.com/grammarly/rocker

通过引入新命令,Rocker旨在解决以下用例,这是普通Docker的痛苦之处:

  1. 在构建阶段挂载可重用的卷,这样依赖管理工具就可以在构建之间使用缓存。
  2. 与build共享ssh密钥(用于提取私有回购等),同时不将它们留在生成的映像中。
  3. 在不同的映像中构建和运行应用程序,能够轻松地将工件从一个映像传递到另一个映像,理想情况下,在一个Dockerfile中有这个逻辑。
  4. 从Dockerfiles直接标记/推送图像。
  5. 通过shell构建命令传递变量,以便它们可以被替换到Dockerfile中。

和更多。这些是阻碍我们在Grammarly采用Docker的最关键的问题。

更新:根据Github上的官方项目回购,Rocker已经停产

截至2018年初,容器生态系统比三年前这个项目启动时要成熟得多。现在,虽然有些特性仍然是rocker独有的,但docker构建或其他支持良好的工具可以很容易地覆盖rocker的一些关键和突出的特性。更多细节参见https://github.com/grammarly/rocker/issues/199

它很丑,但我做到了这样的外观:

Dockerfile:

FROM foo
COPY ./m2/ /root/.m2
RUN stuff

imageBuild.sh:

docker build . -t barImage
container="$(docker run -d barImage)"
rm -rf ./m2
docker cp "$container:/root/.m2" ./m2
docker rm -f "$container"

我有一个java构建,下载宇宙到/root/。,并且这样做每一次imageBuild.sh在构建后将该文件夹的内容复制到主机上,而Dockerfile则将它们复制回映像中以进行下一次构建。

这类似于卷的工作方式(即它在构建之间持续存在)。

首先,回答“为什么VOLUME不能工作?”当你在Dockerfile中定义VOLUME时,你只能定义卷的目标,而不能定义卷的源。在构建过程中,您只能从中获得一个匿名卷。该匿名卷将在每个RUN命令时被挂载,预填充映像的内容,然后在RUN命令结束时被丢弃。只保存对容器的更改,不保存对卷的更改。


既然已经提出了这个问题,一些功能已经发布,可能会有所帮助。首先是多阶段构建,允许您构建磁盘空间效率低下的第一个阶段,并将所需的输出复制到交付的最后一个阶段。第二个特性是Buildkit,它极大地改变了图像构建的方式,并将新的功能添加到构建中。

对于多阶段构建,你会有多个FROM行,每一行开始创建一个单独的映像。默认情况下,只有最后一个图像被标记,但您可以从以前的阶段复制文件。标准的使用方法是使用编译器环境来构建二进制文件或其他应用程序工件,并使用运行时环境作为复制工件的第二个阶段。你可以有:

FROM debian:sid as builder
COPY export /export
RUN compile command here >/result.bin


FROM debian:sid
COPY --from=builder /result.bin /result.bin
CMD ["/result.bin"]

这将导致构建只包含生成的二进制文件,而不包含完整的/export目录。


Buildkit将在18.09推出实验版。它完全重新设计了构建过程,包括更改前端解析器的能力。解析器的其中一个变化是实现了RUN --mount选项,该选项允许您为运行命令挂载缓存目录。例如,这里有一个挂载一些debian目录(重新配置debian镜像,这可以加快包的重新安装):

# syntax = docker/dockerfile:experimental
FROM debian:latest
RUN --mount=target=/var/lib/apt/lists,type=cache \
--mount=target=/var/cache/apt,type=cache \
apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
git

你可以为你拥有的任何应用程序缓存调整缓存目录,例如$HOME/。M2代表maven,或/root/。缓存golang。


使用RUN --mount语法,你还可以从构建上下文绑定挂载只读目录。该文件夹必须存在于构建上下文中,并且没有映射回主机或构建客户端:

# syntax = docker/dockerfile:experimental
FROM debian:latest
RUN --mount=target=/export,type=bind,source=export \
process export directory here...

注意,因为目录是从上下文挂载的,所以它也是只读挂载的,您不能将更改推回主机或客户端。构建时,您需要18.09或更新版本的安装,并使用export DOCKER_BUILDKIT=1启用buildkit。

如果您得到不支持挂载标志的错误,这表明您没有使用上述变量启用buildkit,或者您没有在Dockerfile顶部的语法行(包括注释)之前启用实验性语法。请注意,只有当docker安装有内置的buildkit支持时,切换buildkit的变量才有效,这需要docker的18.09或更新版本,无论是在客户端还是服务器端。

下面是使用构建和提交的两步方法的简化版本,没有shell脚本。它包括:

  1. 部分构建映像,没有卷
  2. 运行容器用卷,进行更改,然后提交结果,替换原来的图像名称。

对于相对较小的更改,额外的步骤只增加了几秒钟的构建时间。

基本上:

docker build -t image-name . # your normal docker build


# Now run a command in a throwaway container that uses volumes and makes changes:
docker run -v /some:/volume --name temp-container image-name /some/post-configure/command


# Replace the original image with the result:
# (reverting CMD to whatever it was, otherwise it will be set to /some/post-configure/command)
docker commit --change="CMD bash" temp-container image-name


# Delete the temporary container:
docker rm temp-container

在我的用例中,我想预先生成一个maven toolchains.xml文件,但是我的许多JDK安装都在运行时才可用的卷上。我的一些映像并不与所有JDKS兼容,因此我需要在构建时测试兼容性,并有条件地填充toolchains.xml。注意,我不需要图像是可移植的,我没有将它发布到Docker Hub。

正如许多人已经回答的那样,在构建期间挂载主机卷是不可能的。我只是想以一种方式添加docker-compose,我认为它会很好,主要用于开发/测试用途

Dockerfile

FROM node:10
WORKDIR /app
COPY . .
RUN npm ci
CMD sleep 999999999

docker-compose.yml

version: '3'
services:
test-service:
image: test/image
build:
context: .
dockerfile: Dockerfile
container_name: test
volumes:
- ./export:/app/export
- ./build:/app/build

并通过docker-compose up -d --build运行容器

如果你正在寻找一种“mount”的方法;文件,比如-v用于docker run,你现在可以对docker build使用--secret标志

echo 'WARMACHINEROX' > mysecret.txt
docker build --secret id=mysecret,src=mysecret.txt .

在Dockerfile中,你可以访问这个秘密

# syntax = docker/dockerfile:1.0-experimental
FROM alpine


# shows secret from default secret location:
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret


# shows secret from custom secret location:
RUN --mount=type=secret,id=mysecret,dst=/foobar cat /foobar

更多关于——secret的深入信息可在Docker Docs上获得

Git克隆从一个私人回购在docker使用我的私人SSH密钥

在BuildKit中创建Linux映像时,docker build也使用--ssh标志提供SSH代理转发。正如Docker的使用SSH访问构建中的私有数据中所记录的,Dockerfile可以在RUN命令中使用--mount=type=ssh,将SSH身份验证委托给主机的SSH代理:

# syntax=docker/dockerfile:1
FROM alpine
RUN apk add --no-cache openssh-client git


# Download public key of remote server at github.com
RUN mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts


# Enable verbose output (optional)
ENV GIT_SSH_COMMAND='ssh -Tvv'


# Clone using private keys known to SSH agent on the host
RUN --mount=type=ssh git clone git@github.com:myorg/myproject.git myproject

在构建时,上面的程序基本上可以通过简单地运行来使用主机的SSH密钥:

docker build --ssh default .

在主机上,这需要使用ssh-add进行一些配置,如上面链接的文档中所述;如果一切正常,那么echo $SSH_AGENT_SOCKecho $SSH_AUTH_SOCK中的一个应该会给你一些输出,并且ssh-add -L应该显示上述可用的标识。

使用详细日志记录时可能出现的一些错误:

  • Host key verification failed:您没有将远程主机添加到映像的~/.ssh/known_hosts
  • pubkey_prepare: ssh_get_authentication_socket: No such file or directory:你忘记在docker build中包含--ssh default参数
  • pubkey_prepare: ssh_get_authentication_socket: Permission denied:你正在使用一些USER <username>吗?然后指定它的用户id 使用--mount=type=ssh,uid=<uid>,或者使用--mount=type=ssh,mode=0666向所有用户打开套接字

这也适用于PIP/Conda/Mamba 直接从版本控制安装Python依赖项,如git+ssh://git@github.com/myorg/myproject.git@mybranch#egg=myproject:

RUN --mount=type=ssh mamba env create -n myenv --file conda_environment.yml

由于Windows容器仍然不支持BuildKit,一种替代方法是使用网络共享。这将创建一个最小的Docker映像大小,因为安装程序是在单个RUN语句期间从容器中添加/删除的。

  • 将EXE / MSI文件放在主机上的一个文件夹中
  • 共享文件夹
  • 开始构建Docker
  • 从Docker容器内映射共享
  • 复制+运行+删除每个安装程序
  • 移除容器内的映射
  • Docker构建
  • 取消主机上的共享文件夹

下面是一个创建Java 11 Windows容器的工作示例:

build.cmd:

host=%COMPUTERNAME%
set domainAndUser=whoami
set "pswd=<host-password>"
set "shareName=tmpSmbShare"
set netPath=\\%COMPUTERNAME%\%shareName%
set "localPath=C:/Users/tester/Desktop/docker/Java11/winsrvcore/installers"


powershell New-SmbShare -Name %shareName% -Path %localPath% -FullAccess "Everyone"
docker build ^
--build-arg domainAndUser=%domainAndUser% ^
--build-arg pswd=%pswd% ^
--build-arg netPath=%netPath% ^
-t <docker-username>/java-11.0.16-winsrvcore.ltsc2019:1.0 .
powershell Remove-SmbShare -Name %shareName% -Force

Dockerfile:

FROM mcr.microsoft.com/windows/servercore:ltsc2019-amd64


ARG domainAndUser
ARG pswd
ARG netPath


RUN powershell New-SMBMapping -LocalPath "R:" -RemotePath "$env:netPath" -UserName "$env:domainAndUser" -Password "$env:pswd" \
&& dir "R:/" \
&& copy "R:/jdk-11.0.16_windows-x64_bin.exe" "C:/" \
&& powershell Start-Process -filepath 'C:/jdk-11.0.16_windows-x64_bin.exe' -Wait -PassThru -ArgumentList "/s,/L,install64.log" \
&& powershell Remove-SMBMapping -LocalPath "R:" -Force \
&& del "C:/jdk-11.0.16_windows-x64_bin.exe"


ENV JAVA_HOME "C:\Program Files\Java\jdk-11.0.16"