Docker部署简单目标检测应用

969 阅读8分钟

上一篇博客介绍了如何搭建完整的应用,实现前端上传图片传给后台处理,后台将检测结果图片返给前端展示。这篇介绍如何把应用部署在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文件内容如下:

配置文件中locationrootalias的区别参考: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并不表示容器本身,而是主机。根据后端应用容器的运行参数,主机映射到后端应用容器townweb8080端口,完成服务调用。通过以下两个方式进一步验证

  • 验证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查看应用日志,确认已经调用到了服务:

为什么在前端页面调用却报超时错误,而且后端应用日志现在根本没有调到服务,原因需要进一步分析。

3.3 自定义Docker网络

参考资料:容器互联 - Docker —— 从入门到实践