实战:使用IDEA构建SpringBoot程序镜像

这里我们创建一个新的SpringBoot项目,现在我们希望能够使用Docker快速地将我们的SpringBoot项目部署到安装了Docker的服务器上,我们就可以将其打包为一个Docker镜像。

image-20220701173902376

先创建好一个项目让它跑起来,可以正常运行就没问题了,接着我们需要将其打包为Docker镜像,这里创建一个新的Dockerfile:

dockerfile 复制代码
FROM ubuntu
RUN apt update && apt install -y openjdk-8-jdk

首先还是基于ubuntu构建一个带Java环境的系统镜像,接着我们先将其连接到我们的Docker服务器进行构建,由于IDEA自带了Docker插件,所以我们直接点击左上角的运行按钮,选择第二项 “为Dockerfile构建镜像”

image-20220701203741495
image-20220701202537650

这里需要配置Docker的服务器,也就是我们在Ubuntu服务器安装的Docker,这里我们填写服务器相关信息,我们首选需要去修改一下Docker的一些配置,开启远程客户端访问:

sh 复制代码
sudo vim /etc/systemd/system/multi-user.target.wants/docker.service 

打开后,添加高亮部分:

image-20220701202846707

修改完成后,重启Docker服务,如果是云服务器,记得开启2375 TCP连接端口:

sh 复制代码
sudo systemctl daemon-reload
sudo systemctl restart docker.service 

现在接着在IDEA中进行配置:

image-20220701203318098

在引擎API URL处填写我们Docker服务器的IP地址:

复制代码
tcp://IP:2375

显示连接成功后,表示配置正确,点击保存即可,接着就开始在我们的Docker服务器上进行构建了:

image-20220701203518930

最后成功构建:

image-20220701204815069

可以看到,Docker服务器上已经有了我们刚刚构建好的镜像:

image-20220701204900943

不过名称没有指定,这里我们重新配置一下:

image-20220701204955570
image-20220701205053642

重新进行构建,就是我们自定义的名称了:

image-20220701205402607
image-20220701205350004

我们来创建一个容器试试看:

image-20220701205500494

好了,现在基本环境搭建好了,我们接着就需要将我们的SpringBoot项目打包然后再容器启动时运行了,打开Maven执行打包命令:

image-20220701205630885

接着我们需要编辑Dockerfile,将我们构建好的jar包放进去:

dockerfile 复制代码
COPY target/DockerTest-0.0.1-SNAPSHOT.jar app.jar

这里需要使用COPY命令来将文件拷贝到镜像中,第一个参数是我们要拷贝的本地文件,第二个参数是存放在Docker镜像中的文件位置,由于还没有学习存储管理,这里我们直接输入app.jar直接保存在默认路径即可。

接着我们就需要指定在启动时运行我们的Java程序,这里使用CMD命令来完成:

dockerfile 复制代码
FROM ubuntu
RUN apt update && apt install -y openjdk-8-jdk
COPY target/DockerTest-0.0.1-SNAPSHOT.jar app.jar
CMD java -jar app.jar
# EXPOSE 8080

CMD命令可以设定容器启动后执行的命令,EXPOSE可以指定容器需要暴露的端口,但是现在我们还没有学习网络相关的知识,所以暂时不使用,这里指定为我们启动Java项目的命令。配置完成后,重新构建:

image-20220701210438145

可以看到历史中已经出现新的步骤了:

image-20220701213513862

接着启动我们的镜像,我们可以直接在IDEA中进行操作,不用再去敲命令了,有点累:

image-20220701210845768
image-20220701210908997

启动后可以在右侧看到容器启动的日志信息:

image-20220701210946261
image-20220701211029119

但是我们发现启动之后并不能直接访问,这是为什么呢?这是因为容器内部的网络和外部网络是隔离的,我们如果想要访问容器内的服务器,需要将对应端口绑定到宿主机上,让宿主主机也开启这个端口,这样才能连接到容器内:

sh 复制代码
docker run -p 8080:8080 -d springboot-test:1.0

这里-p表示端口绑定,将Docker容器内的端口绑定到宿主机的端口上,这样就可以通过宿主的8080端口访问到容器的8080端口了(有关容器网络管理我们还会在后面进行详细介绍),-d参数表示后台运行,当然直接在IDEA中配置也是可以的:

image-20220701211536598

配置好后,点击重新创建容器:

image-20220701211701640

重新运行后,我们就可以成功访问到容器中运行的SpringBoot项目了:

image-20220701211753962

当然,为了以后方便使用,我们可以直接将其推送到Docker Hub中,这里我们还是创建一个新的公开仓库:

image-20220701212330425

这次我们就使用IDEA来演示直接进行镜像的上传,直接点击:

image-20220701212458851

接着我们需要配置一下我们的Docker Hub相关信息:

image-20220701212637581
image-20220701212731276

OK,远程镜像仓库配置完成,直接推送即可,等待推送完成。

image-20220701212902977

可以看到远程仓库中已经出现了我们的镜像,然后IDEA中也可以同步看到:

image-20220701213026214

这样,我们就完成了使用IDEA将SpringBoot项目打包为Docker镜像。


容器网络管理

注意: 本小节学习需要掌握部分《计算机网络》课程中的知识。

前面我们学习了容器和镜像的一些基本操作,了解了如何通过镜像创建容器、然后自己构建容器,以及远程仓库推送等,这一部分我们接着来讨论容器的网络管理。

容器网络类型

Docker在安装后,会在我们的主机上创建三个网络,使用network ls命令来查看:

sh 复制代码
docker network ls
image-20220702161742741

可以看到默认情况下有bridgehostnone这三种网络类型(其实有点像虚拟机的网络配置,也是分桥接、共享网络之类的),我们先来依次介绍一下,在开始之前我们先构建一个镜像,默认的ubuntu镜像由于啥软件都没有,所以我们把一会网络要用到的先提前装好:

sh 复制代码
docker run -it ubuntu
sh 复制代码
apt update
apt install net-tools iputils-ping curl

这样就安装好了,我们直接退出然后将其构建为新的镜像:

sh 复制代码
docker commit lucid_sammet ubuntu-net
image-20220702170441267

OK,一会我们就可以使用了。

  • none网络: 这个网络除了有一个本地环回网络之外,就没有其他的网络了,我们可以在创建容器时指定这个网络。

    这里使用--network参数来指定网络:

    sh 复制代码
    docker run -it --network=none ubuntu-net

    进入之后,我们可以直接查看一下当前的网络:

    sh 复制代码
    ifconfig

    可以看到只有一个本地环回lo网络设备:

    image-20220702170000617

    所以这个容器是无法连接到互联网的:

    image-20220702170531312

    “真”单机运行,可以说是绝对的安全,没人能访问进去,存点密码这些还是不错的。

  • bridge网络: 容器默认使用的网络类型,这是桥接网络,也是应用最广泛的网络类型:

    实际上我们在宿主主机上查看网络信息,会发现有一个名为docker0的网络设备:

    image-20220702172102410

    这个网络设备是Docker安装时自动创建的虚拟设备,它有什么用呢?我们可以来看一下默认创建的容器内部的情况:

    sh 复制代码
    docker run -it ubuntu-net
    image-20220702172532004

    可以看到容器的网络接口地址为172.17.0.2,实际上这是Docker创建的虚拟网络,就像容器单独插了一根虚拟的网线,连接到Docker创建的虚拟网络上,而docker0网络实际上作为一个桥接的角色,一头是自己的虚拟子网,另一头是宿主主机的网络。

    网络拓扑类似于下面这样:

    image-20220702173005750

    通过添加这样的网桥,我们就可以对容器的网络进行管理和控制,我们可以使用network inspect命令来查看docker0网桥的配置信息:

    sh 复制代码
    docker network inspect bridge
    image-20220702173431530

    这里的配置的子网是172.17.0.0,子网掩码是255.255.0.0,网关是172.17.0.1,也就是docker0这个虚拟网络设备,所以我们上面创建的容器就是这个子网内分配的地址172.17.0.2了。

    之后我们还会讲解如何管理和控制容器网络。

  • host网络: 当容器连接到此网络后,会共享宿主主机的网络,网络配置也是完全一样的:

    sh 复制代码
    docker run -it --network=host ubuntu-net

    可以看到网络列表和宿主主机的列表是一样的,不知道各位有没有注意到,连hostname都是和外面一模一样的:

    image-20220702170754656

    只要宿主主机能连接到互联网,容器内部也是可以直接使用的:

    image-20220702171041631

    这样的话,直接使用宿主的网络,传输性能基本没有什么折损,而且我们可以直接开放端口等,不需要进行任何的桥接:

    sh 复制代码
     apt install -y systemctl nginx
     systemctl start nginx

    安装Nginx之后直接就可以访问了,不需要开放什么端口:

    image-20220702171550979

    相比桥接网络就方便得多了。

我们可以根据实际情况,来合理地选择这三种网络使用。

用户自定义网络

除了前面我们介绍的三种网络之外,我们也可以自定义自己的网络,让容器连接到这个网络。

Docker默认提供三种网络驱动:bridgeoverlaymacvlan,不同的驱动对应着不同的网络设备驱动,实现的功能也不一样,比如bridge类型的,其实就和我们前面介绍的桥接网络是一样的。

我们可以使用network create来试试看:

sh 复制代码
docker network create --driver bridge test

这里我们创建了一个桥接网络,名称为test:

image-20220702180837819

可以看到新增了一个网络设备,这个就是一会负责我们容器网络的网关了,和之前的docker0是一样的:

sh 复制代码
docker network inspect test
image-20220702181150667

这里我们创建一个新的容器,使用此网络:

sh 复制代码
 docker run -it --network=test ubuntu-net
image-20220702181252137

成功得到分配的IP地址,是在这个网络内的,注意不同的网络之间是隔离的,我们可以再创建一个容器试试看:

image-20220702181808792

可以看到不同的网络是相互隔离的,无法进行通信,当然我们也为此容器连接到另一个容器所属的网络下:

sh 复制代码
docker network connect test 容器ID/名称
image-20220702182050204

这样就连接了一个新的网络:

image-20220702182146049

可以看到容器中新增了一个网络设备连接到我们自己定义的网络中,现在这两个容器在同一个网络下,就可以相互ping了:
image-20220702182310008

这里就不介绍另外两种类型的网络了,他们是用于多主机通信的,目前我们只学习单机使用。

容器间网络

我们首先来看看容器和容器之间的网络通信,实际上我们之前已经演示过ping的情况了,现在我们创建两个ubuntu容器:

sh 复制代码
docker run -it ubuntu-net

先获取其中一个容器的网络信息:

image-20220702175353454

我们可以直接在另一个容器中ping这个容器:

image-20230814155603447

可以看到能够直接ping通,因为这两个容器都是使用的bridge网络,在同一个子网中,所以可以互相访问。

我们可以直接通过容器的IP地址在容器间进行通信,只要保证两个容器处于同一个网络下即可,虽然这样比较方便,但是大部分情况下,容器部署之后的IP地址是自动分配的(当然也可以使用--ip来手动指定,但是还是不方便),我们无法提前得知IP地址,那么有没有一直方法能够更灵活一些呢?

我们可以借助Docker提供的DNS服务器,它就像是一个真的DNS服务器一样,能够对域名进行解析,使用很简单,我们只需要在容器启动时给个名字就行了,我们可以直接访问这个名称,最后会被解析为对应容器的IP地址,但是注意只会在我们用户自定义的网络下生效,默认的网络是不行的:

sh 复制代码
docker run -it --name=test01 --network=test ubuntu-net
docker run -it --name=test02 --network=test ubuntu-net

接着直接ping对方的名字就可以了:

image-20220702192457354

可以看到名称会自动解析为对应的IP地址,这样的话就不用担心IP不确定的问题了。

当然我们也可以让两个容器同时共享同一个网络,注意这里的共享是直接共享同一个网络设备,两个容器共同使用一个IP地址,只需要在创建时指定:

sh 复制代码
docker run -it --name=test01 --network=container:test02 ubuntu-net

这里将网络指定为一个容器的网络,这样两个容器使用的就是同一个网络了:

image-20220702200711351

可以看到两个容器的IP地址和网卡的Mac地址是完全一样的,它们的网络现在是共享状态,此时在容器中访问,localhost,既是自己也是别人。

我们可以在容器1中,安装Nginx,然后再容器2中访问:

sh 复制代码
 apt install -y systemctl nginx
 systemctl start nginx
image-20220702201348722

成功访问到另一个容器中的Nginx服务器。

容器外部网络

前面我们介绍了容器之间的网络通信,我们接着来看容器与外部网络的通信。

首先我们来看容器是如何访问到互联网的,在默认的三种的网络下,只有共享模式和桥接模式可以连接到外网,共享模式实际上就是直接使用宿主主机的网络设备连接到互联网,这里我们主要来看一下桥接模式。

通过前面的学习,我们了解到桥接模式实际上就是创建一个单独的虚拟网络,让容器在这个虚拟网络中,然后通过桥接器来与外界相连,那么数据包是如何从容器内部的网络到达宿主主机再发送到互联网的呢?实际上整个过程中最关键的就是依靠NAT(Network Address Translation)将地址进行转换,再利用宿主主机的IP地址发送数据包出去。

这里我们就来补充一下《计算机网络》课程中学习的NAT:

实际上NAT在我们生活中也是经常见到的,比如我们要访问互联网上的某个资源,要和服务器进行通信,那么就需要将数据包发送出去,同时服务器也要将数据包发送回来,我们可以知道服务器的IP地址,也可以直接去连接,因为服务器的IP地址是暴露在互联网上的,但是我们的局域网就不一样了,它仅仅局限在我们的家里,比如我们连接了家里的路由器,可以得到一个IP地址,但是你会发现,这个IP公网是无法直接访问到我们的,因为这个IP地址仅仅是一个局域网的IP地址,俗称内网IP,既然公网无法访问到我们,那服务器是如何将数据包发送给我们的呢?

image-20220702230700124

实际上这里就借助了NAT在帮助我们与互联网上的服务器进行通信,通过NAT,可以实现将局域网的IP地址,映射为对应的公网IP地址,而NAT设备一端连接外网,另一端连接内网的所有设备,当我们想要与外网进行通信时,就可以将数据包发送给NAT设备,由它来将数据包的源地址映射为它在外网上的地址,这样服务器就能够发现它了,能够直接与它建立通信。当服务器发送数据回来时,也是直接交给NAT设备,然后再根据地址映射,转发给对应的内网设备(当然由于公网IP地址有限,所以一般采用IP+端口结合使用的形式ANPT)

所以你打开百度直接搜IP,会发现这个IP地址并不是你本地的,而是NAT设备的公网地址:

image-20220702231458928

实际上我们家里的路由器一般都带有NAT功能,默认开启NAT模式,包括我们的小区也是有一个NAT设备在进行转换的,这样你的电脑才能在互联网的世界中遨游。当然NAT也可以保护内网的设备不会直接暴露在公网,这样也会更加的安全,只有当我们主动发起连接时,别人才能知道我们。

当然,我们的Docker也是这样的,实际上内网的数据包想要发送到互联网上去,那么就需要经过这样的一套流程:

image-20220702232449520

这样,Docker容器使用的内网就可以和外网进行通信了。

但是这样有一个问题,单纯依靠NAT的话,只有我们主动与外界联系时,外界才能知道我们,但是现在我们的容器中可能会部署一些服务,需要外界来主动连接我们,此时该怎么办呢?

我们可以直接在容器时配置端口映射,还记得我们在第一节课部署Nginx服务器吗?

sh 复制代码
docker run -d -p 80:80 nginx

这里的-p参数实际上是进行端口映射配置,端口映射可以将容器需要对外提供服务的端口映射到宿主主机的端口上,这样,当外部访问到宿主主机的对应端口时,就会直接转发给容器内映射的端口了。规则为宿主端口:容器端口,这里配置的是将容器的80端口映射到宿主主机的80端口上。

image-20220702233420287

一旦监听到宿主主机的80端口收到了数据包,那么会直接转发给对应的容器。所以配置了端口映射之后,我们才可以从外部正常访问到容器内的服务:

image-20220630165440751

我们也可以直接输入docker ps查看端口映射情况:

image-20220702233831651

至此,有关容器的网络部分,就到此为止,当然这仅仅是单机下的容器网络操作,在以后的课程中,我们还会进一步学习多主机下的网络配置。


容器存储管理

前面我们介绍了容器的网络管理,我们现在已经了解了如何配置容器的网络,以及相关的一些原理。还有一个比较重要的部分就是容器的存储,在这一小节我们将深入了解容器的存储管理。

容器持久化存储

我们知道,容器在创建之后,实际上我们在容器中创建和修改的文件,实际上是被容器的分层机制保存在最顶层的容器层进行操作的,为了保护下面每一层的镜像不被修改,所以才有了这样的CopyOnWrite特性。但是这样也会导致容器在销毁时数据的丢失,当我们销毁容器重新创建一个新的容器时,所有的数据全部丢失,直接回到梦开始的地方。

在某些情况下,我们可能希望对容器内的某些文件进行持久化存储,而不是一次性的,这里就要用到数据卷(Data Volume)了。

在开始之前我们先准备一下实验要用到的镜像:

sh 复制代码
docker run -it ubuntu
apt update && apt install -y vim

然后打包为我们一会要使用的镜像:

复制代码
docker commit 

我们可以让容器将文件保存到宿主主机上,这样就算容器销毁,文件也会在宿主主机上保留,下次创建容器时,依然可以从宿主主机上读取到对应的文件。如何做到呢?只需要在容器启动时指定即可:

sh 复制代码
mkdir test

我们现在用户目录下创建一个新的test目录,然后在里面随便创建一个文件,再写点内容:

sh 复制代码
vim test/hello.txt

接着我们就可以将宿主主机上的目录或文件挂载到容器的某个目录上:

sh 复制代码
docker run -it -v ~/test:/root/test ubuntu-volume

这里用到了一个新的参数-v,用于指定文件挂载,这里是将我们刚刚创建好的test目录挂在到容器的/root/test路径上。

image-20220703105256049

这样我们就可以直接在容器中访问宿主主机上的文件了,当然如果我们对挂载目录中的文件进行编辑,那么相当于编辑的是宿主主机的数据:

sh 复制代码
vim /root/test/test.txt  
image-20220703105626105

在宿主主机的对应目录下,可以直接访问到我们刚刚创建好的文件。

接着我们来将容器销毁,看看当容器不复存在时,挂载的数据时候还能保留:

image-20220703105847329

可以看到,即使我们销毁了容器,在宿主主机上的文件依然存在,并不会受到影响,这样的话,当我们下次创建新的镜像时,依然可以使用这些保存在外面的文件。

比如我们现在想要部署一个Nginx服务器来代理我们的前端,就可以直接将前端页面保存到宿主主机上,然后通过挂载的形式让容器中的Nginx访问,这样就算之后Nginx镜像有升级,需要重新创建,也不会影响到我们的前端页面。这里我们来测试一下,我们先将前端模板上传到服务器:

sh 复制代码
scp Downloads/moban5676.zip 192.168.10.10:~/

然后在服务器上解压一下:

sh 复制代码
unzip moban5676.zip

接着我们就可以启动容器了:

sh 复制代码
docker run -it -v ~/moban5676:/usr/share/nginx/html/ -p 80:80 -d nginx

这里我们将解压出来的目录,挂载到容器中Nginx的默认站点目录/usr/share/nginx/html/(由于挂在后位于顶层,会替代镜像层原有的文件),这样Nginx就直接代理了我们存放在宿主主机上的前端页面,当然别忘了把端口映射到宿主主机上,这里我们使用的镜像是官方的nginx镜像。

现在我们进入容器将Nginx服务启动:

sh 复制代码
systemctl start nginx

然后通过浏览器访问看看是否代理成功:

image-20220703111937254

可以看到我们的前端页面直接被代理了,当然如果我们要编写自定义的配置,也是使用同样的方法操作即可。

注意如果我们在使用-v参数时不指定宿主主机上的目录进行挂载的话,那么就由Docker来自动创建一个目录,并且会将容器中对应路径下的内容拷贝到这个自动创建的目录中,最后挂在到容器中,这种就是由Docker管理的数据卷了(docker managed volume)我们来试试看:

sh 复制代码
docker run -it -v /root/abc ubuntu-volume

注意这里我们仅仅指定了挂载路径,没有指定宿主主机的对应目录,继续创建:

image-20220703112702067

创建后可以看到root目录下有一个新的abc目录,那么它具体是在宿主主机的哪个位置呢?这里我们依然可以使用inspect命令:

sh 复制代码
docker inspect bold_banzai 
image-20220703113507320

可以看到Sorce指向的是/var/lib中的某个目录,我们可以进入这个目录来创建一个新的文件,进入之前记得提升一下权限,权限低了还进不去:

image-20220703114333446

我们来创一个新的文本文档:

image-20220703114429831

实际上和我们之前是一样的,也是可以在容器中看到的,当然删除容器之后,数据依然是保留的。当我们不需要使用数据卷时,可以进行删除:

image-20220703145011638

当然有时候为了方便,可能并不需要直接挂载一个目录上去,仅仅是从宿主主机传递一些文件到容器中,这里我们可以使用cp命令来完成:

image-20220703115648195

这个命令支持从宿主主机复制文件到容器,或是从容器复制文件到宿主主机,使用方式类似于Linux自带的cp命令。

正在加载页面,请稍后...