如何从Docker容器中控制主机?
例如,如何执行复制到主机的bash脚本?
这个答案只是对Bradford Medeiros的解决方案的更详细说明,对我来说也是最好的答案,因此应该归功于他。
在他的回答中,他解释了要做什么(命名管道),但没有确切地说明如何做。
当我阅读他的解决方案时,我必须承认我不知道什么是命名管道。所以我费尽了心思去实现它(其实很简单),但我成功了。所以我的答案的重点就是详细说明你需要运行哪些命令才能使它运作,但是再次感谢他。
在主机上选择您想要放置命名管道文件的文件夹,例如/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中。
如果您像我一样同时使用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容器,并且它完美地工作。
以下是我如何从我的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);
shell_exec('echo "mkdir -p /mydir" > /path/mypipe')
但是没有起作用。有什么想法吗? - JanuszO使用命名管道。
在主机操作系统上,创建一个循环读取命令的脚本,然后在该命名管道上调用 eval
。
让Docker容器读取该命名管道。
为了能够访问该管道,您需要通过卷挂载它。
这与SSH机制(或类似套接字的方法)类似,但可以正确地限制到主机设备,这可能更好。此外,您不必传递身份验证信息。
我唯一的警告是要谨慎考虑为什么要这样做。如果您想创建一个具有用户输入的自升级方法或其他内容,则完全可以这样做,但您可能不希望调用某个命令来获取某些配置数据,因为正确的方法是将其作为参数/卷传递到 Docker 中。此外,请注意您正在执行评估操作,因此请考虑权限模型。
一些其他答案,例如运行脚本。在卷下工作得不通用,因为它们无法访问完整的系统资源,但根据您的使用情况,这可能更合适。
我使用的解决方案是通过SSH
连接到主机,并执行以下命令:
ssh -l ${USERNAME} ${HOSTNAME} "${SCRIPT}"
随着这个答案不断获得赞,我想提醒(并强烈建议):用于调用脚本的帐户应该是一个没有任何权限的帐户,只能使用sudo
来执行该脚本(可以从sudoers
文件中完成)。
我上面提出的解决方案只是我在相对新手时使用的方法。现在到了2021年,可以看一下那些讨论命名管道的答案。这似乎是一个更好的解决方案。
然而,没有人提到安全性。将评估通过管道发送的命令的脚本(调用eval
的脚本)实际上必须不要对整个管道输出使用eval
,而是处理特定情况并根据发送的文本调用所需的命令,否则可能会通过管道发送任何可以做任何事情的命令。
apt update && apt install openssh-client
。 - Mohammed Noureldin那真的取决于你需要这个 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
不过现在你需要深入了解脚本的具体操作,以便从容器内部允许它在主机上使用所需的特定权限。
/usr/bin
暴露给容器。在这两种情况下,容器都没有完全访问主机系统的权限。也许我错了,但这似乎是对一个不好的问题的不好回答。 - Paulmybashscript.sh
回显比如 MAC 地址(某些硬件特定的内容),即使它在容器中被调用,输出是否与我直接在主机终端上运行脚本时相同?或者这种方法只是让我访问脚本,输出将完全像在容器中运行脚本一样? - user5063151我的懒惰导致我找到了这里没有发布的最简单的解决方案。
它基于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。
该设置具有重大安全影响,应谨慎使用(如果有必要)。
编写一个简单的 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()
subprocess.Popen
会在容器中 运行 脚本,而不是在主机上,对吗?(无论脚本的源代码是在主机上还是在容器中。) - ArjanPopen
也会在容器中执行该命令。但是,如果您从主机运行上述脚本,则Popen
将在主机上执行该命令。 - barney765-p 8080:8080
应该是docker
命令的一部分,从容器中发布该API的端口,让我认为它应该在容器中运行(并且subprocess.Popen
应该通过容器在主机上运行)。 (未来的读者,请参见如何从docker容器访问主机端口。) - Arjandocker 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
/usr/bin/docker:/usr/bin/docker
)。 - Gerry我发现使用命名管道找到答案真的很棒。但我想知道是否有一种方法可以获得执行命令的输出。
解决方案是创建两个命名管道:
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
# on the container
echo "ls -l" > /path/to/pipe/exec_in
cat /path/to/pipe/exec_out
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"
docker cp
文档。
一旦文件被复制,您可以在本地运行它。myvalue=$(docker run -it ubuntu echo $PATH)
,并在脚本 shell 中定期测试它(当然,你会使用除 $PATH 之外的其他内容,这只是一个例子)。当它达到特定的值时,你就可以启动你的脚本。 - user2915097docker run --detach-keys="ctrl-p" -it -v /:/mnt/rootdir --name testing busybox
# chroot /mnt/rootdir
#
docker-compose.yml
和几个其他文件的文件夹 2.我git-clone这个仓库,cd进入它的目录并启动docker-compose up
3.结果我获得: -一个具有nginx/php-fpm/mysql等的Web服务器 -工作目录与我的主机系统上的项目代码 -…还将挂载到Web服务器上的某个文件夹。我相信获取项目代码意味着必须在Dockerfile内从主机运行几个命令? - pilat