上一篇博客介绍了如何搭建完整的应用,实现前端上传图片传给后台处理,后台将检测结果图片返给前端展示。这篇介绍如何把应用部署在Docker容器中,涉及以下3个内容:
- SpringBoot后端应用的打包和Docker部署
- Docker部署Nginx访问静态资源文件,即在浏览器中以
ip:port/接口名的形式打开html格式的文件 - 两个容器之间的连接访问,Nginx容器中html文件访问另一个容器中后端应用接口。
1. 后端应用部署
1.1 应用打包
SpringBoot应用大多用来开发前后端分离的后端部分,因此应用打包一般直接打成jar包而不是war包。
1.使用SpringBoot自带spring-boot-maven-plugin插件来打包。在pom.xml文件加入以下配置:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2.在IDEA中Terminal窗口使用mvn clean package打包,打包完成后会在工程的target目录下生成[项目名]-0.0.1-SNAPSHOT.jar的jar包。
3.验证镜像。打开命令行窗口,使用java -jar [项目名]-0.0.1-SNAPSHOT.jar 就可以直接运行文件。
1.2 镜像制作
镜像是docker容器的基础,容器是镜像运行时的实体,镜像和容器的关系类似于面向对象编程语言中类和实例。
DockerHub提供许多镜像,比如java jdk、mysql等。docker提供了以Dockerfile配置文件来定制镜像的方式。
要实现在在容器中部署我们的web应用,必须定制一个镜像,然后再运行定制的镜像。上一节SpringBoot应用打包所得的jar包含了应用所依赖的库,但是不包含jdk,因此基于打包所得的jar包和jdk来定制镜像,步骤如下:
1.新建文件夹djlweb,在该文件夹下新建镜像构建文件Dockerfile
# 基于哪个镜像,需要用一个已有的镜像做载体来创建,这里使用java8镜像
FROM java:8
# 将本地文件夹挂载到当前容器,指定/tmp目录并持久化到Docker数据文件夹,因为Spring Boot使用的内嵌Tomcat容器默认使用/tmp作为工作目录
VOLUME /tmp
# 添加自己的项目到 app.jar。 app.jar的名字可以更改,只要后面几行名字和这个统一就好了
COPY springboot-dl-cv-0.0.1-SNAPSHOT.jar app.jar
# 运行过程中创建一个app.jar文件
RUN bash -c 'touch /app.jar'
# 开放8009端口,只是声明作用,实际还是运行镜像是-p参数为准
EXPOSE 8009
# ENTRYPOINT指定容器运行后默认执行的命令
ENTRYPOINT ["java","-jar","/app.jar"]
2.根据镜像配置文件Dockerfile制作镜像,在命令行窗口运行:
#最后的.表示读取当前路径下的Dockerfile来定制镜像
docker build -t djlweb .
#查看制作后的镜像信息
docker iamges
1.3 镜像运行
在命令行窗口运行:
# -d表示后台运行镜像
# -p 9090:8080表示主机的9090端口映射都容器的8080端口,可以不设置. springboot应用默认开发8080端口
# --name表示镜像运行所得容器的名称, 可以不设置
docker run -d -p 9090:8080 --name townweb djlweb
在win10下的docker desktop中可以查看镜像运行的日志信息:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.4.1)
2021-01-10 02:54:58.062 INFO 1 --- [ main] c.town.djl.cv.SpringbootDlCvApplication : Starting SpringbootDlCvApplication v0.0.1-SNAPSHOT using Java 1.8.0_111 on 89a57fb604ae with PID 1 (/app.jar started by root in /)
2021-01-10 02:54:58.066 INFO 1 --- [ main] c.town.djl.cv.SpringbootDlCvApplication : No active profile set, falling back to default profiles: default
2021-01-10 02:54:59.429 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2021-01-10 02:54:59.443 INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2021-01-10 02:54:59.444 INFO 1 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.41]
2021-01-10 02:54:59.513 INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2021-01-10 02:54:59.514 INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1358 ms
2021-01-10 02:54:59.783 INFO 1 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2021-01-10 02:55:00.241 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2021-01-10 02:55:00.257 INFO 1 --- [ main] c.town.djl.cv.SpringbootDlCvApplication : Started SpringbootDlCvApplication in 2.65 seconds (JVM running for 3.22)
查看容器的运行状态,在命令行下运行:docker ps
1.4 应用部署验证
访问部署在容器中应用的API接口。需要访问主机的9090端口,这样才能映射到容器的8080端口。如果访问主机的8080端口,会提示Error: connect ECONNREFUSED 127.0.0.1:8080
注:docker desktop中可查看应用的日志。由于应用jar包中没有包含目标检测Deep Java Library依赖的C/C++库,因此首次调用时还需要先下载到容器中。
在下载过程中,出现过以下错误。目前还没有找到原因,通过重启主机、重新制作镜像并运行,偶然解决了。后面一方面要继续分析这个错误的原因,另一方法研究怎么在应用打包时把依赖的C/C++库一起打包。
o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.StackOverflowError] with root cause
2. Nginx部署&静态资源访问
参考资料:Docker 部署Nginx 并且挂载默认请求路径和配置文件
2.1 镜像制作
1.配置镜像构建文件Dockerfile
DockerHub官方nginx镜像中nginx.conf配置文件路径是 /etc/nginx/nginx.conf,default.conf配置文件的路径是 /etc/nginx/conf.d/default.conf, 默认首页文件夹html路径是 /usr/share/nginx/html, 日志文件路径是 /var/log/nginx。
#基于dockerhub官方nginx镜像做载体来创建
FROM nginx:latest
#将本地的nginx配置文件覆盖原始镜像中nginx配置文件
COPY ./conf.d/default.conf /etc/nginx/conf.d/default.conf
#将本地的html文件负责到nginx中html下, 是静态资源访问的目标文件
COPY ./html/objectdetection.html /usr/share/nginx/html/objectdetection.html
EXPOSE 8090
CMD ["nginx", "-g", "daemon off;"]
default.conf文件内容如下:
配置文件中location中root和alias的区别参考:Nginx一个server配置多个location
# 原始文件内容
server {
listen 80;
listen [::]:80;
server_name localhost;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
#新增内容
server {
listen 7070;
server_name localhost;
location /detection { #映射关系
alias /usr/share/nginx/html;
index objectdetection.html;
}
}
objectdetection.html实现的是上传本地图片,传给后端进行目标检测,后端将检测结果返给前端展示。文件内容如下,详细信息可以参考:SpringBoot+JS搭建简单目标检测应用
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Object Detection Demo</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js">
</script>
</head>
<body>
<h4>Please chose an image to process object detection</h4>
<input id="browse_file" type="file" accept="image/*" onchange="showImg(this)"/>
<button id="detect_click" onclick="detectImg()">detect</button>
<div id="main" style="width:100%;">
<div id="left" style="width:50%;float:left;">
<p>origal image</p>
<img id="origal_img" src="" alt="" width="" height="">
</div>
<div id="right" style="width:50%;float:left;">
<p>detected image</p>
<img id="detected_img" src="" alt="" width="" height="">
</div>
</div>
<script>
function detectImg() {
console.log("object detection start...");
var file=$('#browse_file')[0].files[0];
var formData=new FormData();
formData.append('file', file);
console.log("formData:", formData.get('file'));
$.ajax({
url : "http://localhost:8080/detectshow/formdata",
type : 'POST',
data : formData,
processData : false, //必须false才会避开jQuery对 formdata 的默认处理
contentType : false, //必须false才会自动加上正确的Content-Type
success : function(result) { //jquery请求返回的结果好像都是字符串类型
console.log("reponse result:", result);
var src = 'data:image/png;base64,' + result;
$("#detected_img").attr('src', src);
$("#detected_img").css("width", "70%");
$("#detected_img").css("height", "70%");
},
error : function(result) {
console.log("reponse result:", result);
alert("Post Faile!");
}
});
console.log("object detection end!");
}
function showImg(obj) { //预览图片
var file=$(obj)[0].files[0]; //获取文件信息
var formData=new FormData();
formData.append('img',file); //只是前端预览, 属性名称可以随便定义
if(file) {
var reader=new FileReader(); //调用FileReader
reader.readAsDataURL(file); //将文件读取为 DataURL(base64)
reader.onload=function(evt){ //读取操作完成时触发。
$("#origal_img").attr('src', evt.target.result); //将img标签的src绑定为DataURL
$("#origal_img").css('width', "70%");
$("#origal_img").css('height', "70%");
};
}else {
alert("上传失败");
}
}
</script>
</body>
</html>
2.根据镜像配置文件Dockerfile制作镜像
#最后的.表示读取当前路径下的Dockerfile来定制镜像
docker build -t nginxweb .
#查看制作后的镜像信息
docker iamges
2.2 镜像运行
在命令行运行:
#将容器的80端口映射到主机的80端口
docker run -d -p 80:80 -p 7070:7070 --name townnginx nginxweb
docker ps
进入容器内部,可看到容器内/usr/share/nginx/html文件夹下有我们复制过去的objectdetection.html文件
2.3 静态资源访问
在主机的浏览器通过http://localhost:7070/detection就可以访问nginx容器下静态资源文件/usr/share/nginx/html/objectdetection.html,结果如下:
http://localhost:7070/detection
3. 容器互联
参考资料:Docker容器互访三种方式
3.1 容器->主机->容器
将2.1节中objectdetection.html文件里后台服务的url修改为:
$.ajax({
url : "http://localhost:9090/detectshow/formdata",
...
});
发现前端是可以调用到townweb容器中后端接口,从后端应用日志和前端页面效果都可以确认,结果如下:
原因分析:Nginx容器townnginx调用localhost:9090时,其中localhost并不表示容器本身,而是主机。根据后端应用容器的运行参数,主机映射到后端应用容器townweb的8080端口,完成服务调用。通过以下两个方式进一步验证
- 验证1:将
objectdetection.html文件里后台服务的url改为直接用主机的ip地址:
$.ajax({
url : "http://172.30.64.1:9090/detectshow/formdata",
...
});
调用依然成功,说明容器可以直接访问主机网络。
- 验证2:将
objectdetection.html文件里后台服务的url改为直接用nginx容器的ip地址:
$.ajax({
url : "http://172.17.0.3:9090/detectshow/formdata",
...
});
注:通过docker inspect 容器id可以查得Nginx容器的ip地址是172.17.0.3
这时调用失败,说明容器内的localhost并不是指向容器的本身,而是指向主机。
3.2 容器->容器
直接通过容器ip互联,具体步骤如下:
将objectdetection.html文件里后台服务的url改为后端服务的地址,ip用后端应用所在容器的ip:
$.ajax({
url : "http://172.17.0.2:8080/detectshow/formdata", //端口也改为后端应用容器的服务开放端口
...
});
但是页面调用时报连接超时(failed) net::ERR_CONNECTION_TIMED_OUT
在容器townnginx命令行却是可以调用的:
在容器townweb查看应用日志,确认已经调用到了服务:
为什么在前端页面调用却报超时错误,而且后端应用日志现在根本没有调到服务,原因需要进一步分析。