0 缘起
上次使用Expect+SSH实现特定容器的重启操作,但是这一次需要查询容器状态,展示在页面上,然后还要通过按钮实现容器的start/stop/restart操作,这每一个容器每一个指令都这么来一遍吧,虽说也可以,但是不够优雅,所以经过我的不断搜索,我找到了一个更优雅的办法——Dockers Remote API。哦绝对不是我没试出来Portainer API才退而求其次用Dockers Remote API的,也不是因为有Dockers Remote API的视频教程。
1 Docker Remote API
首先,要设置Docker API的端口,编辑
/usr/lib/systemd/system/docker.service
文件中,
ExecStart=/usr/bin/dockerd
一行的末尾添加
-H tcp://0.0.0.0:<port>
可以选择一个无冲突的端口号作为Docker API的端口,保存后记得重启Docker守护进程和docker。
$ systemctl daemon-reload$ systemctl restart docker
当我们完成后,在服务器上就可以通过
$ curl localhost:<port>/containers/json
来获取运行中容器的信息了。
当然,这只是完成了在本机上,或者是局域网内通过终端能够获取运行中容器的信息,距离真正利用上Docker Remote API还有一段距离,更别提通过按钮和网页进行交互了。
2 CORS跨域问题
同源策略是一个浏览器重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
浏览器的同源策略,同源——即“协议protocol/主机host/端口port”这个元组都相同的话,浏览器才允许交互,而显而易见的是,处于宿主机的Docker API的端口,和容器中的WebUI服务端口,是不一样的……所以如何解决跨域问题……当然正确的做法是修改你的服务器端,添加
Access-Control-Allow-Origin
标头,指定为*,或者发起请求的那个就行了。但是……Docker API你让我去哪里找这个啊……
拿一个MDN给出例子来说的话:
下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:
| URL | 结果 | 原因 |
| http://store.company.com/dir2/other.htm | 同源 | 只有路径不同 |
| http://store.company.com/dir/inner/another.html | 同源 | 只有路径不同 |
| https://store.company.com/secure.html | 失败 | 协议不同 |
| http://store.company.com:81/dir/etc.html | 失败 | 端口不同 ( http:// 默认口是80) |
| http://news.company.com/dir/other.html | 失败 | 主机不同 |
网上给出了许多方案,比如用Nginx反向代理把请求转发,当然也可以,就是不够优(jian)雅(dan),Nginx容器又需要部署,又需要设置,还是挺麻烦的。这不我找到了一个CORS—anywhere的简单方便的方法,只要按着github上面的,把仓库拉取到本地,进入文件夹运行npm install,前提是你装了nodejs,然后修改下文件server.js里面端口,运行起来node server.js,就可以了,终端会打印出相关信息,把想要跨域域名+端口加在后面就可以绕过跨域问题了。
万事大吉,let's测试!
3 Apifox
按理来说测试Api要使用大名鼎鼎的Postman,但是官网下载了v9的版本打开之后又让我升级v10,打开v10又告诉我你要的功能v10不支持,你得退回v9,想了想这么麻烦,不如我找个替代的吧,一搜还真有不少,Apifox宣称自己Apifox = Postman + Swagger + Mock + JMeter,“API 文档、API 调试、API Mock、API 自动化测试”,还是个国产的,那决定就是你了!
blabla一通操作进入快捷请求里,选择请求类型为“GET”,地址填上CORS—anywhere反向代理的地址加上Docker Remote API的地址,点击发送。
结果并没有如第一步中使用curl那样获得运行中容器的信息,那是出了什么问题呢?看看相应信息——响应体中的内容是:
Missing required request header. Must specify one of: origin,x-requested-with
缺了个请求头?那填上试试。在Apifox请求里面加上请求头
{'x-requested-with':'XMLHttpRequest'}
来表示这是个ajax的请求,再一次点击发送,这次成功了。
那接下来就是测试容器的控制了,将请求类型设定为“POST”,API按照Docker官方文档中写的/containers/{id}/start、/containers/{id}/stop、/containers/{id}/restart对应容器的启动、停止和重启命令,一一测试验证查看容器状态,发现一切就如预期。
一同操作猛如虎,终于完成了往WebUI上编写的全部前置条件……
4 HTML+JS
在HTML上写上几个td单元格来展示状态——哦因为我为了偷懒不想算css所以全丢进表格里面了,给上各自的id方便我们的DOM操作,按钮一通加,不要吝啬。
js呢就有一些复杂了。
首先使在页面加载完成后进行一次状态查询,就是要把容器状态从Docker Remote API那里获取到,然后判断相应的容器是否存在,是否是在运行的状态,最后是修改相关的单元格里的内容,简单点就是一个span,因为它是行内元素,不然还要考虑换行啊float啊,就不优雅。
那咱们上代码:
document.addEventListener("DOMContentLoaded", function () {$.ajax({methods: "get",url: "{CORS-anywher反向代理的地址}" + "{Docker Remote API的地址}/containers/json",success: function (data) {for (key in data) {switch (data[key].Names[0]) {case "{server镜像的名称}":if (data[key].State == "running") {server_status_content.innerHTML ="<span style='color: green;'>已连接</span>";}break;case "{client镜像的名称}":if (data[key].State == "running") {client_status_content.innerHTML ="<span style='color: green;'>已连接</span>";}break;}}},});})
我觉得可行,咱们打开这个页面看一看——怎么没有状态显示??哦我没有设定初始值……加上加上:
var server_status_content = document.getElementById("server_status");server_status_content.innerHTML = "<span style='color: red;'>未连接</span>";var client_status_content = document.getElementById("client_status");client_status_content.innerHTML = "<span style='color: red;'>未连接</span>";
接着绑定按钮,咱这次用原生JS来:、
var start_server_btn = document.getElementById("start_server");start_server_btn.addEventListener("click", () => {var xhr = new XMLHttpRequest();xhr.open("post","{CORS-anywher反向代理的地址}" + "{Docker Remote API的地址}/containers/{server容器名称}/start");xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");xhr.send();xhr.onload = function () {console.log(xhr);};});
但是这又会出现一个问题,就是点击完这个按钮,容器状态的改变不会引发前面状态展示的更新,还需要手动刷新一次页面才行,这可不行,我们要重新查询一次容器状态,最好还是有一个延时,免得服务器反应不及23333
这样一来就要对之前的代码进行一个复用,一但代码需要复用,咱就是一个考虑封装成一个函数,就命名为refresh_status好了,然后立刻进行一个调用:
function refresh_status() {var server_status_content = document.getElementById("server_status");server_status_content.innerHTML = "<span style='color: red;'>未连接</span>";var client_status_content = document.getElementById("client_status");client_status_content.innerHTML = "<span style='color: red;'>未连接</span>";jQuery.ajax({methods: "get",url: "{CORS-anywher反向代理的地址}" + "{Docker Remote API的地址}/containers/json",success: function (data) {for (key in data) {switch (data[key].Names[0]) {case "{server镜像的名称}":if (data[key].State == "running") {server_status_content.innerHTML ="<span style='color: green;'>已连接</span>";}break;case "{client镜像的名称}":if (data[key].State == "running") {client_status_content.innerHTML ="<span style='color: green;'>已连接</span>";}break;}}},});}refresh_status()
于是按钮绑定的事件也进行一下更新:
start_server_btn.addEventListener("click", () => {var xhr = new XMLHttpRequest();xhr.open("post","{CORS-anywher反向代理的地址}" + "{Docker Remote API的地址}/containers/{server容器名称}/start");xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");xhr.send();xhr.onload = function () {var delay = setTimeout(refresh_status(), 100);clearTimeout(delay);};});
5 PM2进程管理
最近经历了一波网络整顿,可能路由器的超时时间设置的特别短,一会儿就连接中断了,而这些Node.js的脚本又是跟随终端进程的,也就意味着连接中断,node脚本也就被结束运行了,还得重新登录服务器,重新输入命令运行脚本,脚本比较少的时候还没什么不变,好几个主机好几个脚本就很让人苦恼了,所以不得不求助PM2进行进程管理。
在终端输入
$ npm install pm2@latest -g
安装完成后再输入
$ pm2 start {app.js}
使用pm2守护这个脚本,然后再设置开机自启动:
$ pm2 save$ pm2 startup
妈妈再也不用担心进程中断啦。




