如何从Docker容器中运行Shell脚本到主机?

206

如何从Docker容器中控制主机?

例如,如何执行复制到主机的bash脚本?


13
那不会正好相反于将主机与Docker隔离吗? - Marcus Müller
107
可以的。但有时是必要的。 - Alex Ushakov
2
可能是 在 docker 容器内执行主机命令 的重复问题。 - Marcus Müller
不确定“控制主机”的含义,但我最近参加了一场由数据科学家主持的讲座,他们正在使用Docker运行脚本来处理大量工作负载(使用AWS挂载的GPU),并将结果输出到主机。这是一个非常有趣的用例。基本上,这些脚本通过Docker打包在可靠的执行环境中。 - KCD
也许我试图以错误的方式完成它,但以下是我想要实现的目标: 1.有一个“Docker软件包”,位于某个仓库中,其中包含一个带有docker-compose.yml和几个其他文件的文件夹 2.我git-clone这个仓库,cd进入它的目录并启动docker-compose up 3.结果我获得: -一个具有nginx/php-fpm/mysql等的Web服务器 -工作目录与我的主机系统上的项目代码 -…还将挂载到Web服务器上的某个文件夹。我相信获取项目代码意味着必须在Dockerfile内从主机运行几个命令? - pilat
显示剩余2条评论
15个回答

186

这个答案只是对Bradford Medeiros的解决方案的更详细说明,对我来说也是最好的答案,因此应该归功于他。

在他的回答中,他解释了要做什么(命名管道),但没有确切地说明如何做。

当我阅读他的解决方案时,我必须承认我不知道什么是命名管道。所以我费尽了心思去实现它(其实很简单),但我成功了。所以我的答案的重点就是详细说明你需要运行哪些命令才能使它运作,但是再次感谢他。

PART 1 - 测试没有docker的命名管道概念

在主机上选择您想要放置命名管道文件的文件夹,例如/path/to/pipe/和一个管道名称,例如mypipe,然后运行:

mkfifo /path/to/pipe/mypipe

管道已创建。 类型

ls -l /path/to/pipe/mypipe 

检查以“p”开头的访问权限,例如

prw-r--r-- 1 root root 0 mypipe

现在运行:

tail -f /path/to/pipe/mypipe

终端现在正在等待将数据发送到此管道中

现在打开另一个终端窗口。

然后运行:

echo "hello world" > /path/to/pipe/mypipe

检查第一个终端(带有tail -f的那个),它应该显示“hello world”

第二部分-通过管道运行命令

在主机容器上,不要运行tail -f,它只会输出作为输入发送的任何内容,运行这个命令将其作为命令执行:

eval "$(cat /path/to/pipe/mypipe)"

然后,从另一个终端尝试运行:

echo "ls -l" > /path/to/pipe/mypipe

回到第一个终端,你应该能看到 ls -l 命令的结果。

第三部分 - 让它永远监听

你可能已经注意到,在上一部分中,在显示 ls -l 输出后,它停止了监听命令。

不要运行 eval "$(cat /path/to/pipe/mypipe)",而是运行:

while true; do eval "$(cat /path/to/pipe/mypipe)"; done

(你可以使用 nohup 命令来做到这一点)

现在,您可以发送无限数量的命令,一个接一个地执行,不仅仅是第一个。

第四部分 - 即使发生重新启动也能使其工作

唯一的问题是如果主机必须重新启动,“while”循环将停止工作。

为了处理重新启动,这是我做的:

while true; do eval "$(cat /path/to/pipe/mypipe)"; done放入名为execpipe.sh的文件中,并带有#!/bin/bash

别忘了为它添加chmod +x

通过运行以下命令将其添加到crontab中:

crontab -e

然后添加

@reboot /path/to/execpipe.sh

现在测试一下:重新启动您的服务器,当它重新运行时,请将一些命令回显到管道中并检查它们是否被执行。

当然,您无法看到命令的输出,因此ls -l无法帮助您,但touch somefile会有所帮助。

另一个选项是修改脚本以将输出放入文件中,例如:

while true; do eval "$(cat /path/to/pipe/mypipe)" &> /somepath/output.txt; done

现在您可以运行ls -l,使用bash中的&>将输出(包括stdout和stderr)保存到output.txt中。

第5部分-让它与Docker一起工作

如果您像我一样同时使用docker compose和dockerfile,则可以按照以下方式操作:

假设您想将mypipe的父文件夹挂载为容器中的/hostpipe,请添加以下内容:

VOLUME /hostpipe

在您的Dockerfile中创建挂载点的方法是:

然后添加以下内容:

volumes:
   - /path/to/pipe:/hostpipe

在你的docker-compose文件中,为了将/path/to/pipe挂载为/hostpipe,需要进行修改。

重启你的docker容器。

第六部分 - 测试

进入你的docker容器:

docker exec -it <container> bash

进入挂载文件夹,检查是否可以看到管道:

cd /hostpipe && ls -l

现在尝试在容器内部运行命令:

echo "touch this_file_was_created_on_main_host_from_a_container.txt" > /hostpipe/mypipe

应该可以工作!

警告:如果您的主机是OSX(Mac OS),而容器是Linux,则不会起作用(解释在此处 https://dev59.com/nJHea4cB1Zd3GeqPm0WQ#43474708,问题在此处 https://github.com/docker/for-mac/issues/483),因为管道实现不同,因此从Linux写入管道的内容只能由Linux读取,而从Mac OS写入管道的内容只能由Mac OS读取(这句话可能不太准确,但请注意跨平台问题存在)。

例如,当我从我的Mac OS计算机上运行我的Docker设置时,在DEV中,如上所述的命名管道不起作用。但在staging和production中,我有Linux主机和Linux容器,并且它完美地工作。

第7部分 - 来自Node.JS容器的示例

以下是我如何从我的Node.JS容器发送命令到主机并检索输出:

const pipePath = "/hostpipe/mypipe"
const outputPath = "/hostpipe/output.txt"
const commandToRun = "pwd && ls-l"

console.log("delete previous output")
if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath)

console.log("writing to pipe...")
const wstream = fs.createWriteStream(pipePath)
wstream.write(commandToRun)
wstream.close()

console.log("waiting for output.txt...") //there are better ways to do that than setInterval
let timeout = 10000 //stop waiting after 10 seconds (something might be wrong)
const timeoutStart = Date.now()
const myLoop = setInterval(function () {
    if (Date.now() - timeoutStart > timeout) {
        clearInterval(myLoop);
        console.log("timed out")
    } else {
        //if output.txt exists, read it
        if (fs.existsSync(outputPath)) {
            clearInterval(myLoop);
            const data = fs.readFileSync(outputPath).toString()
            if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath) //delete the output file
            console.log(data) //log the output of the command
        }
    }
}, 300);

3
可以。您可以创建一个名为“dockeruser”的用户,该用户仅具备运行Docker命令的权限,并确保在容器内运行时使用该用户执行这些命令。此外,要注意安全性问题,以免在容器内部启动和停止其他容器时出现潜在的漏洞。 - Kristof van Woensel
1
@Vincent,你知道如何在PHP中运行命令吗?我尝试了shell_exec('echo "mkdir -p /mydir" > /path/mypipe')但是没有起作用。有什么想法吗? - JanuszO
2
当然,该命令在容器中可以工作,但无法从 PHP 代码中执行。 - JanuszO
5
荣誉归于他,但我的投票给了你。 - Ken Ratanachai S.
1
谢谢!现在如果我需要从命令中读取结果怎么办?我们能创建一个双向管道吗? - user3240688
显示剩余9条评论

147

使用命名管道。

在主机操作系统上,创建一个循环读取命令的脚本,然后在该命名管道上调用 eval

让Docker容器读取该命名管道。

为了能够访问该管道,您需要通过卷挂载它。

这与SSH机制(或类似套接字的方法)类似,但可以正确地限制到主机设备,这可能更好。此外,您不必传递身份验证信息。

我唯一的警告是要谨慎考虑为什么要这样做。如果您想创建一个具有用户输入的自升级方法或其他内容,则完全可以这样做,但您可能不希望调用某个命令来获取某些配置数据,因为正确的方法是将其作为参数/卷传递到 Docker 中。此外,请注意您正在执行评估操作,因此请考虑权限模型。

一些其他答案,例如运行脚本。在卷下工作得不通用,因为它们无法访问完整的系统资源,但根据您的使用情况,这可能更合适。


46
请注意:这是正确/最佳答案,需要更多的赞扬。其他答案都在纠结于询问“你想做什么”,并对某些内容进行例外处理。我有一个非常特定的用例,需要我能够做到这一点,而这是我认为唯一好的答案。使用SSH需要降低安全/防火墙标准,而Docker运行命令则完全错误。感谢您提供这个答案。我猜这个回答没有得到很多赞,因为它不是一个简单的复制/粘贴答案,但这就是答案。如果可能的话,我会给您+100分。 - Farley
5
寻找更多信息的人,可以在主机上使用以下脚本:https://unix.stackexchange.com/a/369465 当然,你需要使用'nohup'来运行它,并创建一种监管包装器以保持其活动状态(也许使用cron job:P)。 - sucotronic
11
这可能是一个不错的回答。但如果您能提供更多细节和一些更详细的命令行解释,那就会更好了。您能否详细说明一下? - Mohammed Noureldin
7
点赞了,这个方法可行! 使用'mkfifo host_executor_queue'命令在挂载的卷上创建一个命名管道。然后添加一个消费者来执行被放入队列中的命令作为主机shell,使用'tail -f host_executor_queue | sh &'。最后,在队列中推送命令使用'echo touch foo > host_executor_queue'——这个测试在主目录下创建一个临时文件foo。 如果您想要让消费者在系统启动时开始运行,请将'@reboot tail -f host_executor_queue | sh &'添加到crontab中。只需添加host_executor_queue的相对路径即可。 - skybunk
3
如果它提供了一个如何做的例子,这将是一个好回答。它只是一个描述,并没有链接到任何相关内容。虽然不是很好的答案,但是可以朝着正确的方向前进。 - TetraDev
显示剩余5条评论

91

我使用的解决方案是通过SSH连接到主机,并执行以下命令:

ssh -l ${USERNAME} ${HOSTNAME} "${SCRIPT}"

更新

随着这个答案不断获得赞,我想提醒(并强烈建议):用于调用脚本的帐户应该是一个没有任何权限的帐户,只能使用sudo来执行该脚本(可以从sudoers文件中完成)。

更新:命名管道

我上面提出的解决方案只是我在相对新手时使用的方法。现在到了2021年,可以看一下那些讨论命名管道的答案。这似乎是一个更好的解决方案。

然而,没有人提到安全性。将评估通过管道发送的命令的脚本(调用eval的脚本)实际上必须不要对整个管道输出使用eval,而是处理特定情况并根据发送的文本调用所需的命令,否则可能会通过管道发送任何可以做任何事情的命令。


作为另一种解决方法,容器可以输出一组命令,主机在容器退出后运行它们:eval $(docker run --rm -it container_name_to_output script)。 - parity3
1
@RonRosenfeld,你使用的是哪个Docker镜像?如果是Debian/Ubuntu,请运行以下命令:apt update && apt install openssh-client - Mohammed Noureldin
@RonRosenfeld,抱歉我不明白你的意思。 - Mohammed Noureldin
@RonRosenfeld,你需要在容器中以某种方式安装OpenSSH。 - Mohammed Noureldin
谢谢。我会尝试弄清楚的。 - Ron Rosenfeld
显示剩余5条评论

35

那真的取决于你需要这个 bash 脚本做什么!

例如,如果这个 bash 脚本只是回显一些输出,你可以这样做:

docker run --rm -v $(pwd)/mybashscript.sh:/mybashscript.sh ubuntu bash /mybashscript.sh

另一个可能性是您希望 bash 脚本安装一些软件-比如安装 docker-compose 的脚本。您可以这样做:

docker run --rm -v /usr/bin:/usr/bin --privileged -v $(pwd)/mybashscript.sh:/mybashscript.sh ubuntu bash /mybashscript.sh

不过现在你需要深入了解脚本的具体操作,以便从容器内部允许它在主机上使用所需的特定权限。


1
我有一个想法,可以制作一个容器,连接到主机并创建新的容器。 - Alex Ushakov
7
第一行启动了一个新的Ubuntu容器,并挂载了脚本,使得容器可以读取该脚本。但是它不允许容器访问主机文件系统。第二行将主机的/usr/bin暴露给容器。在这两种情况下,容器都没有完全访问主机系统的权限。也许我错了,但这似乎是对一个不好的问题的不好回答。 - Paul
3
好的,问题很模糊。问题并没有要求“完全访问主机系统”。如所描述的那样,如果bash脚本只是想回显一些输出,则不需要任何访问主机文件系统的权限。对于我的第二个例子,安装docker-compose,您唯一需要的权限是访问二进制文件存储的bin目录。正如我一开始所说的-为了做到这一点,您必须对脚本正在执行的内容有非常具体的想法,以允许正确的权限。 - Paul Becotte
1
我知道这是一个老问题,但举个例子。如果 mybashscript.sh 回显比如 MAC 地址(某些硬件特定的内容),即使它在容器中被调用,输出是否与我直接在主机终端上运行脚本时相同?或者这种方法只是让我访问脚本,输出将完全像在容器中运行脚本一样? - user5063151
7
尝试了一下,脚本在容器中执行,而不是在主机上执行。 - All2Pie
显示剩余9条评论

34

我的懒惰导致我找到了这里没有发布的最简单的解决方案。

它基于luc juggery精彩文章

要在docker容器内从linux主机获得完整的shell,您需要做的只是:

docker run --privileged --pid=host -it alpine:3.8 \
nsenter -t 1 -m -u -n -i sh

说明:

--privileged:授予容器额外的权限,使其能够访问主机的设备(/dev)

--pid = host:允许容器使用Docker宿主机上的进程树(Docker守护程序运行的虚拟机) nsenter工具:允许在现有的名称空间中运行进程(提供容器隔离的构建块)

nsenter (-t 1 -m -u -n -i sh) 可以在与PID 1进程相同的隔离上下文中运行sh进程。 然后,整个命令将在VM中提供一个交互式的sh shell。

该设置具有重大安全影响,应谨慎使用(如果有必要)。


2
迄今为止最好、最简单的解决方案!谢谢Shmulik提供它(Yashar Koach!) - MMEL
这绝对是最简单的解决方案。 - Waleed
问题是这样允许了容器外的事物在容器内部运行。然而,如果你想要运行位于容器内部的进程,并且允许该进程在主机上看到和运行容器外的命令,这种方法就会失效,因为它无法找到首先在容器中的命令。 - Owl

12

编写一个简单的 Python 服务器并让其监听某个端口(比如8080),将该端口绑定至容器上,对 localhost:8080 发出 HTTP 请求并用 popen 运行 shell 脚本来运行 Python 服务器。可以使用 curl 进行测试或编写代码以发出 HTTP 请求,如:curl -d '{"foo":"bar"}'localhost:8080

#!/usr/bin/python
from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer
import subprocess
import json

PORT_NUMBER = 8080

# This class will handles any incoming request from
# the browser 
class myHandler(BaseHTTPRequestHandler):
        def do_POST(self):
                content_len = int(self.headers.getheader('content-length'))
                post_body = self.rfile.read(content_len)
                self.send_response(200)
                self.end_headers()
                data = json.loads(post_body)

                # Use the post data
                cmd = "your shell cmd"
                p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
                p_status = p.wait()
                (output, err) = p.communicate()
                print "Command output : ", output
                print "Command exit status/return code : ", p_status

                self.wfile.write(cmd + "\n")
                return
try:
        # Create a web server and define the handler to manage the
        # incoming request
        server = HTTPServer(('', PORT_NUMBER), myHandler)
        print 'Started httpserver on port ' , PORT_NUMBER

        # Wait forever for incoming http requests
        server.serve_forever()

except KeyboardInterrupt:
        print '^C received, shutting down the web server'
        server.socket.close()

1
在我看来,这是最好的答案。在主机上运行任意命令必须通过某种 API(例如 REST)进行。这是唯一可以强制执行安全性并正确控制运行进程(例如杀死、处理 stdin、stdout、退出代码等)的方法。当然,如果这个 API 可以在 Docker 内部运行,那就太好了,但个人认为直接在主机上运行也无妨。 - barney765
请纠正我如果我错了,但是 subprocess.Popen 会在容器中 运行 脚本,而不是在主机上,对吗?(无论脚本的源代码是在主机上还是在容器中。) - Arjan
1
@Arjan,如果您在容器内运行上述脚本,则Popen也会在容器中执行该命令。但是,如果您从主机运行上述脚本,则Popen将在主机上执行该命令。 - barney765
谢谢,@barney765。在主机上运行提供API是有意义的,就像你的第一条评论一样。我想(对我来说)“将端口-p 8080:8080与容器绑定”是令人困惑的部分。我假设-p 8080:8080应该是docker命令的一部分,从容器中发布该API的端口,让我认为它应该在容器中运行(并且subprocess.Popen应该通过容器在主机上运行)。 (未来的读者,请参见如何从docker容器访问主机端口。) - Arjan
这将无法使用绑定端口8080:8080,因为主机上的端口已经被占用,所以无法在8080上启动服务器。 - undefined

6
如果您不担心安全问题,只是想从一个Docker容器内部启动主机上的另一个Docker容器,您可以通过共享其监听套接字来与主机上运行的Docker服务器共享。请查看https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface,并确定您的个人风险承受能力是否允许在特定应用程序中这样做。您可以通过将以下卷参数添加到启动命令来实现此目的。
docker run -v /var/run/docker.sock:/var/run/docker.sock ...

或者通过在您的Docker Compose文件中共享/var/run/docker.sock,像这样:

version: '3'

services:
   ci:
      command: ...
      image: ...
      volumes:
         - /var/run/docker.sock:/var/run/docker.sock


当您在Docker容器中运行docker start命令时,运行在主机上的Docker服务器将看到该请求并提供兄弟容器。
来源:http://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/

1
请注意,在容器中必须安装Docker,否则您还需要挂载Docker二进制文件的卷(例如/usr/bin/docker:/usr/bin/docker)。 - Gerry
1
在容器中挂载docker套接字时,请小心,这可能是一个严重的安全问题:https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface - DatGuyKaj
@DatGuyKaj 谢谢,我已经编辑了我的答案以反映你的资源所提出的问题。 - Matt Bucci
这并没有回答问题,问题是关于在主机上运行脚本,而不是在容器中运行。 - Brandon

4

我发现使用命名管道找到答案真的很棒。但我想知道是否有一种方法可以获得执行命令的输出。

解决方案是创建两个命名管道:

mkfifo /path/to/pipe/exec_in
mkfifo /path/to/pipe/exec_out

然后,使用循环的解决方案,如@Vincent所建议的,将变为:

# on the host
while true; do eval "$(cat exec_in)" > exec_out; done

然后在 Docker 容器中,我们可以使用以下命令执行命令并获取输出:
# on the container
echo "ls -l" > /path/to/pipe/exec_in
cat /path/to/pipe/exec_out

如果有人感兴趣,我的需求是在容器中使用主机上的故障转移IP,我创建了这个简单的Ruby方法:
def fifo_exec(cmd)
  exec_in = '/path/to/pipe/exec_in'
  exec_out = '/path/to/pipe/exec_out'
  %x[ echo #{cmd} > #{exec_in} ]
  %x[ cat #{exec_out} ]
end
# example
fifo_exec "curl https://ip4.seeip.org"

1
正如Marcus所提醒的那样,Docker基本上是进程隔离。从Docker 1.8开始,您可以在主机和容器之间双向复制文件,请参阅docker cp文档。

https://docs.docker.com/reference/commandline/cp/

一旦文件被复制,您可以在本地运行它。

2
我知道该怎样在 Docker 容器中运行这个脚本,换句话说。 - Alex Ushakov
1
重复的内容:https://dev59.com/vlwZ5IYBdhLWcg3wM9_Z#31721604? - user2915097
2
@AlexUshakov:绝对不行。那样做会破坏 Docker 的许多优势。不要这样做。不要尝试。重新考虑你需要做什么。 - Marcus Müller
1
你可以在主机上获取容器中某个变量的值,例如 myvalue=$(docker run -it ubuntu echo $PATH),并在脚本 shell 中定期测试它(当然,你会使用除 $PATH 之外的其他内容,这只是一个例子)。当它达到特定的值时,你就可以启动你的脚本。 - user2915097
1
@MarcusMüller 这是完全可能的,并且存在许多充分的理由来这样做;构建使用Docker容器的规范化环境的作业和测试是一个很好的例子和常见用例(特别是在复杂的跨平台软件构建和/或在具有复杂堆栈的系统上运行集成测试时)。 - Iain Collins
显示剩余2条评论

1
docker run --detach-keys="ctrl-p" -it -v /:/mnt/rootdir --name testing busybox
# chroot /mnt/rootdir
# 

4
虽然这个答案可能解决了提问者的问题,但建议您解释一下它是如何工作的以及为什么能解决这个问题。这有助于新开发人员理解正在发生的事情,并学习如何自行修复类似的问题。感谢您的贡献! - Caleb Kleveter
我们是否可以通过Docker容器运行本地可用的Shell脚本呢? - undefined

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