如何从 Docker 容器在主机上运行 shell 脚本?

如何从码头容器控制主机?

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

190712 次浏览

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

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

一旦复制了文件,就可以在本地运行它

扩展到 User2915097’s 回应:

隔离的思想是能够非常清楚地限制应用程序/进程/容器(无论您从哪个角度看)对主机系统的影响。因此,能够复制和执行一个文件将真正打破整个概念。

是的,但有时候这是必要的。

没有。事实并非如此,否则就不该用 Docker。你应该做的是为你想要做的事情声明一个清晰的接口(例如,更新主机配置) ,然后编写一个最小的客户端/服务器来做 没错,仅此而已。然而,总的来说,这似乎并不是很理想。在许多情况下,您应该简单地重新考虑您的方法并根除这种需求。Docker 出现的时候,基本上所有东西都是可以通过某种协议访问的服务。我想不出 Docker 容器获得在主机上执行任意内容的权限的任何正确用例。

这实际上取决于您需要 bash 脚本做什么!

例如,如果 bash 脚本只是回显一些输出,那么您可以这样做

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

另一种可能性是,您希望 bash 脚本安装某些软件——比如安装 docker 的脚本——撰写。你可以这么做

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

但是在这一点上,您真正需要深入了解脚本正在做什么,以允许它从容器内部对您的主机拥有特定的权限。

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

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

更新

由于这个答案不断得到投票,我想提醒(并强烈推荐) ,用于调用脚本的帐户应该是一个没有任何权限的帐户,但只能以 sudo的形式执行该脚本(可以从 sudoers文件执行)。

更新: 命名管道

上面我建议的解决方案只是我刚接触 Docker 时使用的一个。现在让我们看看2021年关于 名叫派普斯的答案。这似乎是一个更好的解决方案。

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

使用命名管道。 在主机操作系统上,创建一个脚本来循环和读取命令,然后对此调用 eval

让 Docker 容器读取到指定的管道。

为了能够访问管道,您需要通过卷安装它。

这类似于 SSH 机制(或类似的基于套接字的方法) ,但是将您限制在适当的主机设备上,这可能更好。另外,您不必传递身份验证信息。

我唯一的警告是要小心你为什么要这么做。如果您希望创建一个方法来使用用户输入或其他东西进行自我升级,但是您可能不希望调用命令来获取一些配置数据,因为正确的方法是将其作为 args/volux 传递给 docker。另外,要对您正在进行评估的事实保持谨慎,所以只需要考虑一下权限模型。

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

编写一个简单的服务器 Python 服务器监听端口(比如8080) ,将端口 -p 8080:8080与容器绑定,向 localhost: 8080发出 HTTP 请求,要求 Python 服务器使用 popen 运行 shell 脚本,运行 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()
docker run --detach-keys="ctrl-p" -it -v /:/mnt/rootdir --name testing busybox
# chroot /mnt/rootdir
#

我有个简单的方法。

步骤1: Mount/var/run/docker.sock:/var/run/docker.sock (这样您就可以在容器中执行 docker 命令)

步骤2: 在容器中执行以下操作。这里的关键部分是(网络主机,因为它将从主机上下文执行)

Docker run-i —— rm —— network host-v/opt/test.sh:/test.sh alpine: 3.7 Sh/test.sh

Sh 应该包含一些您需要的命令(ifconfig、 netstat 等等)。 现在您将能够获得主机上下文输出。

如果您不担心安全性,只是希望从另一个 docker 容器(如 OP)中启动主机上的 docker 容器,那么您可以通过共享 docker 容器的监听套接字来共享主机上运行的 docker 服务器。

请参阅 https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface,看看您的个人风险容忍度是否允许这个特定的应用程序。

为此,可以将以下卷参数添加到 start 命令中

docker run -v /var/run/docker.sock:/var/run/docker.sock ...

或者像下面这样在你的 docker 组合文件中共享/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/

您可以使用管道的概念,但是可以使用主机上的文件和 监视来实现从一个 docker 容器在主机上执行脚本的目标。如下(使用风险自负) :

#! /bin/bash


touch .command_pipe
chmod +x .command_pipe


# Use fswatch to execute a command on the host machine and log result
fswatch -o --event Updated .command_pipe | \
xargs -n1 -I "{}"  .command_pipe >> .command_pipe_log  &


docker run -it --rm  \
--name alpine  \
-w /home/test \
-v $PWD/.command_pipe:/dev/command_pipe \
alpine:3.7 sh


rm -rf .command_pipe
kill %1

在本例中,容器内部将命令发送到/dev/command _ tube,如下所示:

/home/test # echo 'docker network create test2.network.com' > /dev/command_pipe

在主机上,您可以检查是否创建了网络:

$ docker network ls | grep test2
8e029ec83afe        test2.network.com                            bridge              local

我的懒惰使我找到了一个最简单的解决方案,这个方案并没有在这里作为答案发表。

它是以 Luc Juggery文章不错为基础的。

要从 docker 容器中获得 Linux 主机的完整 shell,您所需要做的就是:

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

说明:

——特权: 授予容器额外的权限,它允许容器访问主机的设备(/dev)

—— pid = host: 允许容器使用 Docker 主机(运行 Docker 守护进程的 VM)的进程树 Nsenter 实用程序: 允许在现有名称空间(为容器提供隔离的构建块)中运行进程

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

此设置具有重大的安全隐患,应谨慎使用(如果有的话)。

这个答案只是一个 Bradford Medeiros 解决方案的更详细的版本,对我来说也是最好的答案,所以功劳归于他。

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

我必须承认,当我读到他的解决方案时,我不知道什么是管道。所以我努力去实现它(虽然它实际上很简单) ,但我确实成功了。 所以我的答案只是详细说明你需要运行哪些命令才能让它正常工作,但是,功劳还是归他。

第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”

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

在主机容器上,不要运行 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

(你做不到)

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

第4部分-使它工作,即使重新启动发生

唯一的警告是,如果主机必须重新启动,“ 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 Comose 和 dockerfile,以下是我所做的:

假设您希望将 mypipe 的父文件夹作为 /hostpipe挂载到容器中

添加以下内容:

VOLUME /hostpipe

在 dockerfile 中创建一个挂载点

然后加上这个:

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

在 docker 组合文件中,以便将/path/to/tube 挂载为/hosttube

重新启动码头集装箱。

第6部-测试

执行到你的码头容器:

docker exec -it <container> bash

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

cd /hostpipe && ls -l

现在尝试从容器中运行一个命令:

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

应该会有用的!

警告: 如果你有一个 OSX (Mac OS)主机和一个 Linux 容器,它不会工作(这里解释 https://stackoverflow.com/a/43474708/10018801和这里发布 https://github.com/docker/for-mac/issues/483) ,因为管道实现是不一样的,所以你从 Linux 写入管道的内容只能被 Linux 读取,而你从 Mac OS 写入管道的内容只能被 Mac OS 读取(这句话可能不太准确,但是要注意存在跨平台问题)。

例如,当我在 Mac OS 计算机上运行 DEV 中的 Docker 设置时,上面解释的命名管道不起作用。但是在准备阶段和生产阶段,我有 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);

在我的场景中,我只需要 ssh 登录容器中的主机(通过主机 ip) ,然后就可以对主机进行任何操作

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

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

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"