在Dockerfile中使用带有“source”的RUN指令无法正常工作。

444
我有一个Dockerfile,我正在编写它来安装一个纯净的Python环境(稍后将在其中安装一个应用程序)。

我有一个Dockerfile,我正在编写它来安装一个纯净的Python环境(稍后将在其中安装一个应用程序)。

FROM ubuntu:12.04

# required to build certain python libraries
RUN apt-get install python-dev -y

# install pip - canonical installation instructions from pip-installer.org
# http://www.pip-installer.org/en/latest/installing.html
ADD https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py /tmp/ez_setup.py
ADD https://raw.github.com/pypa/pip/master/contrib/get-pip.py /tmp/get-pip.py
RUN python /tmp/ez_setup.py
RUN python /tmp/get-pip.py
RUN pip install --upgrade pip 

# install and configure virtualenv
RUN pip install virtualenv 
RUN pip install virtualenvwrapper
ENV WORKON_HOME ~/.virtualenvs
RUN mkdir -p $WORKON_HOME
RUN source /usr/local/bin/virtualenvwrapper.sh

构建一直运行良好,直到最后一行出现以下异常:

[previous steps 1-9 removed for clarity]
...
Successfully installed virtualenvwrapper virtualenv-clone stevedore
Cleaning up...
 ---> 1fc253a8f860
Step 10 : ENV WORKON_HOME ~/.virtualenvs
 ---> Running in 8b0145d2c80d
 ---> 0f91a5d96013
Step 11 : RUN mkdir -p $WORKON_HOME
 ---> Running in 9d2552712ddf
 ---> 3a87364c7b45
Step 12 : RUN source /usr/local/bin/virtualenvwrapper.sh
 ---> Running in c13a187261ec
/bin/sh: 1: source: not found

如果我ls到那个目录(仅为了测试之前的步骤是否已提交),我可以看到文件如预期一样存在:

$ docker run 3a87 ls /usr/local/bin
easy_install
easy_install-2.7
pip
pip-2.7
virtualenv
virtualenv-2.7
virtualenv-clone
virtualenvwrapper.sh
virtualenvwrapper_lazy.sh

如果我尝试仅运行source命令,我将获得与上面相同的“未找到”错误。 但是,如果我运行一个交互式shell会话,则source可以正常工作:

$ docker run 3a87 bash
source
bash: line 1: source: filename argument required
source: usage: source filename [arguments]

我可以从这里运行脚本,然后愉快地访问workonmkvirtualenv等。

经过一些调查,最初看起来问题可能在于Ubuntu登录shell为bash,而Ubuntu系统shell为dash之间的区别,dash不支持source命令。

然而,解决方法似乎是使用'.'代替source,但这会导致Docker运行时出现go panic异常。

在Dockerfile RUN指令中运行shell脚本的最佳方法是什么(我正在使用Ubuntu 12.04 LTS的默认基础镜像)?

19个回答

242

原始答案

FROM ubuntu:14.04
RUN rm /bin/sh && ln -s /bin/bash /bin/sh

这适用于每个Ubuntu docker基础镜像。我通常为我编写的每个Dockerfile添加此行。

由一个关心的旁观者编辑

如果您想要实现“在整个Dockerfile中使用 bash 而不是 sh ”的效果,而不会更改和可能损坏容器内部的操作系统,则可以告诉Docker您的意图。就是这样做的:

SHELL ["/bin/bash", "-c"]

可能的损害是,Linux 中的许多脚本(在 Ubuntu 上进行全新安装时,grep -rHInE '/bin/sh' / 返回超过 2700 个结果)期望在 /bin/sh 中使用完全符合 POSIX 标准的 shell。Bash shell 不仅仅是 POSIX 加上额外的内置命令。有一些内置命令(以及更多其他内容)的行为与 POSIX 完全不同。我完全支持避免使用 POSIX(以及任何你认为避免了 Bashism 的脚本都可以工作的谬论),只使用 Bashism。但你需要在你的脚本中使用适当的 shebang 来实现这一点,而不是从整个操作系统中删除 POSIX shell。(除非你有时间验证 Linux 中附带的所有 2700 多个脚本以及你安装的所有软件包中的脚本。)更多细节请参考此答案。https://dev59.com/wGIj5IYBdhLWcg3wGRiM#45087082

19
这可以稍微简化一下:ln -snf /bin/bash /bin/sh - apottere
37
“ln -s /bin/bash /bin/sh”的意思是“将/bin/sh链接到/bin/bash”,这样做非常不好。Ubuntu之所以将/bin/sh指向dash,是有原因的。Dash是一个完全符合POSIX标准的Shell,比Bash快得多。将/bin/sh链接到Bash会大大降低服务器的性能。引用:http://wiki.ubuntu.com/DashAsBinSh - xero
15
这只是一个不太正规的做法,而不是解决方案。如果你的脚本由 sh shell 运行,但你想使用 bash,正确的解决方案要么是让 sh 进程作为一次性调用 bash,例如 bash -c 'source /script.sh && ...'或者你甚至可以避免使用 bash 特有的语法(比如 source),而是选择只使用有效的 POSIX 等价物,例如 . /script.sh。(注意在 . 后面有空格!)最后,如果你的脚本是可执行的(不仅仅是可源化的),则永远不要使用 #!/bin/sh 做 shebang,如果它实际上不兼容 sh。请改用 #!/bin/bash - Mark G.
3
给关心的旁观者一个椰子。我来这里是为了 SHELL 命令,而不是用 bash 替换 sh。 - Dave

183

RUN 指令的默认 shell 是 ["/bin/sh", "-c"]

RUN "source file"      # translates to: RUN /bin/sh -c "source file"

通过SHELL指令,您可以在Dockerfile中为随后的RUN指令更改默认shell:

SHELL ["/bin/bash", "-c"] 

现在,默认的 shell 已经改变,你不需要在每个 RUN 指令中显式定义它。

RUN "source file"    # now translates to: RUN /bin/bash -c "source file"

附加说明:你还可以添加--login选项,这将启动一个登录 shell。这意味着~/.bashrc会被读取,因此在运行命令之前无需显式地执行它。


80

最简单的方法是使用点操作符代替 source,这是 sh 中等同于 bash 的 source 命令:

而不是:

RUN source /usr/local/bin/virtualenvwrapper.sh

使用:

RUN . /usr/local/bin/virtualenvwrapper.sh

虽然了解如何使用“/bin/sh”来获取shell脚本可能是有益的,但它不如“SHELL ["/bin/bash", "-c"]”自我记录。 - avans

59
如果您使用的是 Docker 1.12 或更新版本,只需使用 SHELL 命令即可。

简短回答:

一般情况下:

SHELL ["/bin/bash", "-c"] 

对于Python虚拟环境:

SHELL ["/bin/bash", "-c", "source /usr/local/bin/virtualenvwrapper.sh"]

详细回答:

来自https://docs.docker.com/engine/reference/builder/#shell

SHELL ["executable", "parameters"]

The SHELL instruction allows the default shell used for the shell form of commands to be overridden. The default shell on Linux is ["/bin/sh", "-c"], and on Windows is ["cmd", "/S", "/C"]. The SHELL instruction must be written in JSON form in a Dockerfile.

The SHELL instruction is particularly useful on Windows where there are two commonly used and quite different native shells: cmd and powershell, as well as alternate shells available including sh.

The SHELL instruction can appear multiple times. Each SHELL instruction overrides all previous SHELL instructions, and affects all subsequent instructions. For example:

FROM microsoft/windowsservercore

# Executed as cmd /S /C echo default
RUN echo default

# Executed as cmd /S /C powershell -command Write-Host default
RUN powershell -command Write-Host default

# Executed as powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello

# Executed as cmd /S /C echo hello
SHELL ["cmd", "/S"", "/C"]
RUN echo hello

The following instructions can be affected by the SHELL instruction when the shell form of them is used in a Dockerfile: RUN, CMD and ENTRYPOINT.

The following example is a common pattern found on Windows which can be streamlined by using the SHELL instruction:

...
RUN powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"
...

The command invoked by docker will be:

cmd /S /C powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"

This is inefficient for two reasons. First, there is an un-necessary cmd.exe command processor (aka shell) being invoked. Second, each RUN instruction in the shell form requires an extra powershell -command prefixing the command.

To make this more efficient, one of two mechanisms can be employed. One is to use the JSON form of the RUN command such as:

...
RUN ["powershell", "-command", "Execute-MyCmdlet", "-param1 \"c:\\foo.txt\""]
...

While the JSON form is unambiguous and does not use the un-necessary cmd.exe, it does require more verbosity through double-quoting and escaping. The alternate mechanism is to use the SHELL instruction and the shell form, making a more natural syntax for Windows users, especially when combined with the escape parser directive:

# escape=`

FROM microsoft/nanoserver
SHELL ["powershell","-command"]
RUN New-Item -ItemType Directory C:\Example
ADD Execute-MyCmdlet.ps1 c:\example\
RUN c:\example\Execute-MyCmdlet -sample 'hello world'

Resulting in:

PS E:\docker\build\shell> docker build -t shell .
Sending build context to Docker daemon 4.096 kB
Step 1/5 : FROM microsoft/nanoserver
 ---> 22738ff49c6d
Step 2/5 : SHELL powershell -command
 ---> Running in 6fcdb6855ae2
 ---> 6331462d4300
Removing intermediate container 6fcdb6855ae2
Step 3/5 : RUN New-Item -ItemType Directory C:\Example
 ---> Running in d0eef8386e97


    Directory: C:\


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       10/28/2016  11:26 AM                Example


 ---> 3f2fbf1395d9
Removing intermediate container d0eef8386e97
Step 4/5 : ADD Execute-MyCmdlet.ps1 c:\example\
 ---> a955b2621c31
Removing intermediate container b825593d39fc
Step 5/5 : RUN c:\example\Execute-MyCmdlet 'hello world'
 ---> Running in be6d8e63fe75
hello world
 ---> 8e559e9bf424
Removing intermediate container be6d8e63fe75
Successfully built 8e559e9bf424
PS E:\docker\build\shell>

The SHELL instruction could also be used to modify the way in which a shell operates. For example, using SHELL cmd /S /C /V:ON|OFF on Windows, delayed environment variable expansion semantics could be modified.

The SHELL instruction can also be used on Linux should an alternate shell be required such as zsh, csh, tcsh and others.

The SHELL feature was added in Docker 1.12.


54

我遇到了同样的问题,为了在虚拟环境中执行pip安装,我必须使用以下命令:

我也遇到了同样的问题,在虚拟环境中执行pip install时,我必须使用这个命令:

RUN pip install virtualenv virtualenvwrapper
RUN mkdir -p /opt/virtualenvs
ENV WORKON_HOME /opt/virtualenvs
RUN /bin/bash -c "source /usr/local/bin/virtualenvwrapper.sh \
    && mkvirtualenv myapp \
    && workon myapp \
    && pip install -r /mycode/myapp/requirements.txt"

我希望这可以帮到你。


30
根据 https://docs.docker.com/engine/reference/builder/#runRUN 的默认[Linux] shell是 /bin/sh -c。看起来你期望使用 bashisms,因此你应该使用“exec form”的 RUN 来指定你的shell。
RUN ["/bin/bash", "-c", "source /usr/local/bin/virtualenvwrapper.sh"]

否则,使用“shell form”运行并指定不同的shell会导致嵌套的shell。
# don't do this...
RUN /bin/bash -c "source /usr/local/bin/virtualenvwrapper.sh"
# because it is the same as this...
RUN ["/bin/sh", "-c", "/bin/bash" "-c" "source /usr/local/bin/virtualenvwrapper.sh"]

如果你有超过1个需要不同shell的命令,你应该阅读https://docs.docker.com/engine/reference/builder/#shell并在你的RUN命令之前加入以下内容来更改默认shell:

SHELL ["/bin/bash", "-c"]

最后,如果你把任何东西放在了root用户的.bashrc文件中,并且你需要它,请在SHELLRUN命令中添加-l标志,使其成为登录 shell 并确保其被调用。

注意:我故意忽略了在RUN命令中只有一个脚本命令的情况下调用它是毫无意义的事实。


28

在此页面的答案基础上,我想补充一点,您必须意识到每个RUN语句都使用/bin/sh -c独立运行,因此不会获得在登录shell中通常会引用的任何环境变量。

目前我发现的最好方法是将脚本添加到 /etc/bash.bashrc,然后作为bash登录调用每个命令。

RUN echo "source /usr/local/bin/virtualenvwrapper.sh" >> /etc/bash.bashrc
RUN /bin/bash --login -c "your command"

例如,您可以安装和设置virtualenvwrapper,创建虚拟环境,在使用bash登录时激活它,然后将Python模块安装到该环境中:

RUN pip install virtualenv virtualenvwrapper
RUN mkdir -p /opt/virtualenvs
ENV WORKON_HOME /opt/virtualenvs
RUN echo "source /usr/local/bin/virtualenvwrapper.sh" >> /etc/bash.bashrc
RUN /bin/bash --login -c "mkvirtualenv myapp"
RUN echo "workon mpyapp" >> /etc/bash.bashrc
RUN /bin/bash --login -c "pip install ..."

阅读关于bash启动文件的手册可以帮助理解何时被调用。


12

6

我在Dockerfile中运行source时也遇到了问题。

这在构建CentOS 6.6 Docker容器时完美运行,但在Debian容器中出现了问题。

RUN cd ansible && source ./hacking/env-setup

这是我解决问题的方式,可能不太优雅,但对我而言很有效。
RUN echo "source /ansible/hacking/env-setup" >> /tmp/setup
RUN /bin/bash -C "/tmp/setup"
RUN rm -f /tmp/setup

6
如果你有可用的SHELL,应该使用这个答案 - 不要使用被接受的答案,因为它会强制你将其余的dockerfile放在一个命令中,参见此评论
如果你正在使用旧版Docker并且没有访问权限SHELL,只要不需要来自.bashrc的任何内容(这在Dockerfile中是很少见的情况),这个方法也可行。
ENTRYPOINT ["bash", "--rcfile", "/usr/local/bin/virtualenvwrapper.sh", "-ci"]

请注意必须使用-i选项,使bash读取rc文件。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接