都24年了还有人不懂docker吗?一问带你熟悉docker
上篇笔记在这里:https://www.codefather.cn/post/1855098402082897922
容器存储管理
前面我们介绍了容器的网络管理,我们现在已经了解了如何配置容器的网络,以及相关的一些原理。还有一个比较重要的部分就是容器的存储,在这一小节我们将深入了解容器的存储管理。
容器持久化存储
我们知道,容器在创建之后,实际上我们在容器中创建和修改的文件,实际上是被容器的分层机制保存在最顶层的容器层进行操作的,为了保护下面每一层的镜像不被修改,所以才有了这样的CopyOnWrite特性。但是这样也会导致容器在销毁时数据的丢失,当我们销毁容器重新创建一个新的容器时,所有的数据全部丢失,直接回到梦开始的地方。
在某些情况下,我们可能希望对容器内的某些文件进行持久化存储,而不是一次性的,这里就要用到数据卷(Data Volume)了。
在开始之前我们先准备一下实验要用到的镜像:
▼sh复制代码docker run -it ubuntu apt update && apt install -y vim
然后打包为我们一会要使用的镜像:
▼text复制代码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路径上。
这样我们就可以直接在容器中访问宿主主机上的文件了,当然如果我们对挂载目录中的文件进行编辑,那么相当于编辑的是宿主主机的数据:
▼sh复制代码vim /root/test/test.txt    在宿主主机的对应目录下,可以直接访问到我们刚刚创建好的文件。    可以看到,即使我们销毁了容器,在宿主主机上的文件依然存在,并不会受到影响,这样的话,当我们下次创建新的镜像时,依然可以使用这些保存在外面的文件。 比如我们现在想要部署一个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
可以看到我们的前端页面直接被代理了,当然如果我们要编写自定义的配置,也是使用同样的方法操作即可。
注意如果我们在使用-v参数时不指定宿主主机上的目录进行挂载的话,那么就由Docker来自动创建一个目录,并且会将容器中对应路径下的内容拷贝到这个自动创建的目录中,最后挂在到容器中,这种就是由Docker管理的数据卷了(docker managed volume)我们来试试看:
▼sh复制代码docker run -it -v /root/abc ubuntu-volume
注意这里我们仅仅指定了挂载路径,没有指定宿主主机的对应目录,继续创建:
创建后可以看到root目录下有一个新的abc目录,那么它具体是在宿主主机的哪个位置呢?这里我们依然可以使用inspect命令:
▼sh复制代码docker inspect bold_banzai
可以看到Sorce指向的是/var/lib中的某个目录,我们可以进入这个目录来创建一个新的文件,进入之前记得提升一下权限,权限低了还进不去:
我们来创一个新的文本文档:
实际上和我们之前是一样的,也是可以在容器中看到的,当然删除容器之后,数据依然是保留的。当我们不需要使用数据卷时,可以进行删除:
这个命令支持从宿主主机复制文件到容器,或是从容器复制文件到宿主主机,使用方式类似于Linux自带的
cp命令。
容器数据共享
前面我们通过挂载的形式,将宿主主机上的文件直接挂载到容器中,这样容器就可以直接访问到宿主主机上的文件了,并且在容器删除时也不会清理宿主主机上的文件。
我们接着来看看如何实现容器与容器之间的数据共享,实际上按照我们之前的思路,我们可以在宿主主机创建一个公共的目录,让这些需要实现共享的容器,都挂载这个公共目录:
▼sh复制代码docker run -it -v ~/test:/root/test ubuntu-volume
由于挂载的是宿主主机上的同一块区域,所以内容可以直接在两个容器中都能访问。当然我们也可以将另一个容器挂载的目录,直接在启动容器时指定使用此容器挂载的目录:
▼sh复制代码docker run -it -v ~/test:/root/test --name=data_test ubuntu-volume docker run -it --volumes-from data_test ubuntu-volume
这里使用--volumes-from指定另一个容器(这种用于给其他容器提供数据卷的容器,我们一般称为数据卷容器)
可以看到,数据卷容器中挂载的内容,在当前容器中也是存在的,当然就算此时数据卷容器被删除,那么也不会影响到这边,因为这边相当于是继承了数据卷容器提供的数据卷,所以本质上还是让两个容器挂载了同样的目录实现数据共享。
虽然通过上面的方式,可以在容器之间实现数据传递,但是这样并不方便,可能某些时候我们仅仅是希望容器之间共享,而不希望有宿主主机这个角色直接参与到共享之中,此时我们就需要寻找一种更好的办法了。其实我们可以将数据完全放入到容器中,通过构建一个容器,来直接将容器中打包好的数据分享给其他容器,当然本质上依然是一个Docker管理的数据卷,虽然还是没有完全脱离主机,但是移植性就高得多了。
我们来编写一个Dockerfile:
▼dockerfile复制代码FROM ubuntu ADD moban5676.tar.gz /usr/share/nginx/html/ VOLUME /usr/share/nginx/html/
这里我们使用了一个新的指令ADD,它跟COPY命令类似,也可以复制文件到容器中,但是它可以自动对压缩文件进行解压,这里只需要将压缩好的文件填入即可,后面的VOLUME指令就像我们使用-v参数一样,会创建一个挂载点在容器中:
▼sh复制代码cd test tar -zcvf moban5676.tar.gz * mv moban5676.tar.gz .. cd ..
接着我们直接构建:
▼sh复制代码docker build -t data .

可以看到所有的文件都自动解压出来了(除了中文文件名称乱码了之外,不过无关紧要)我们退出容器,可以看到数据卷列表中新增了我们这个容器需要使用的:
XIDoj.webp" alt="image-20220703153514730" width="100%" />
这个位置实际上就是数据存放在当前主机上的位置了,不过是由Docker进行管理而不是我们自定义的。现在我们就可以创建一个新的容器直接继承了:
▼sh复制代码docker run -p 80:80 --volumes-from=data_test -d nginx
访问一下Nginx服务器,可以看到成功代理:
这样我们就实现了将数据放在容器中进行共享,我们不需要刻意去指定宿主主机的挂载点,而是Docker自行管理,这样就算迁移主机依然可以快速部署。
容器资源管理
前面我们已经完成Docker的几个主要模块的学习,最后我们来看看如何对容器的资源进行管理。
容器控制操作
在开始之前,我们还是要先补充一些我们前面没有提到的其他容器命令。
首先我们的SpringBoot项目在运行是,怎么查看输出的日志信息呢?
▼sh复制代码docker logs test
这里使用log命令来打印容器中的日志信息:
现在我们的容器已经启动了,但是我们想要进入到容器监控容器的情况怎么办呢?我们可以是
attach命令来附加到容器启动命令的终端上:
▼sh复制代码docker attach 容器ID/名称
ker中运行的Java程序的。
退出后,容器依然是处于运行状态的。
我们也可以使用exec命令在容器中启动一个新的终端或是在容器中执行命令:
▼sh复制代码docker exec -it test bash
-it和run命令的操作是一样的,这里执行后,会创建一个新的终端(当然原本的程序还是在正常运行)我们会在一个新的终端中进行交互:
执行后会在容器中打开一个新的终端执行命令,并输出结果。
前面我们还学习了容器的停止操作,通过输入stop命令来停止容器,但是此操作并不会立即停止,而是会等待容器处理善后,那么怎么样才能强制终止容器呢?我们可以直接使用kill命令,相当于给进程发送SIGKILL信号,强制结束。
▼sh复制代码docker kill test
相比stop命令,kill就没那么温柔了。
有时候可能只是希望容器暂时停止运行,而不是直接终止运行,我们希望在未来的某个时间点,恢复容器的运行,此时就可以使用pause命令来暂停容器:
▼sh复制代码docker pause test
此时处于爱的魔力转圈圈状态,我们可以将其恢复运行,使用
unpause命令:
▼sh复制代码docker unpause test
物理资源管理
对于一个容器,在某些情况下我们可能并不希望它占据所有的系统资源来运行,我们只希望分配一部分资源给容器,比如只分配给容器2G内存,最大只允许使用2G,不允许再占用更多的内存,此时我们就需要对容器的资源进行限制。
▼sh复制代码docker run -m 内存限制 --memory-swap=内存和交换分区总共的内存限制 镜像名称
其中-m参数是对容器的物理内存的使用限制,而--memory-swap是对内存和交换分区总和的限制,它们默认都是-1,也就是说没有任何的限制(如果在一开始仅指定-m参数,那么交换内存的限制与其保持一致,内存+交换等于-m的两倍大小)默认情况下跟宿主主机一样,都是2G内存,现在我们可以将容器的内存限制到100M试试看,其中物理内存50M,交换内存50M,尝试启动一下SpringBoot程序:
▼sh复制代码docker run -it -m 50M --memory-swap=100M nagocoler/springboot-test:1.0
当然除了对内存的限制之外,我们也可以对CPU资源进行限额,默认情况下所有的容器都可以平等地使用CPU资源,我们可以调整不同的容器的CPU权重(默认为1024),来按需分配资源,这里需要使用到
-c选项,也可以输入全名--cpu-share:
▼sh复制代码docker run -c 1024 ubuntu docker run -c 512 ubuntu
这里容器的CPU权重比例为16比8,也就是2比1(注意多个容器时才会生效),那么当CPU资源紧张时,会按照此权重来分配资源,当然如果CPU资源并不紧张的情况下,依然是有机会使用到全部的CPU资源的。
这里我们使用一个压力测试工具来进行验证:
▼sh复制代码docker run -c 1024 --name=cpu1024 -it ubuntu docker run -c 512 --name=cpu512 -it ubuntu
接着我们分别进入容器安装stress压力测试工具:
▼sh复制代码apt update && apt install -y stress
接着我们分别在两个容器中都启动压力测试工具,产生4个进程不断计算随机数的平方根:
▼sh复制代码stress -c 4
可以看到权重高的容器中,分配到了更多的CPU资源,而权重低的容器中,只分配到一半的CPU资源。
当然我们也可以直接限制容器使用的CPU数量:
▼sh复制代码docker run -it --cpuset-cpus=1 ubuntu
制后,只会使用CPU 1的资源了:
可以看到,4个进程只各自使用了25%的CPU,加在一起就是100%,也就是只能占满一个CPU的使用率。如果要分配多个CPU,则使用逗号隔开:
▼sh复制代码docker run -it --cpuset-cpus=0,1 ubuntu
这样就会使用这两个CPU了:
当然也可以直接使用--cpus来限制使用的CPU资源数:
▼sh复制代码docker run -it --cpus=1 ubuntu    限制为1后,只能使用一个CPU提供的资源,所以这里加载一起只有一个CPU的资源了。当然还有更精细的`--cpu-period `和`--cpu-quota`,这里就不做介绍了。 最后我们来看一下对磁盘IO读写性能的限制,我们首先使用`dd`命令来测试磁盘读写速度: ```sh dd if=/dev/zero of=/tmp/1G bs=4k count=256000 oflag=direct
可以看到当前的读写速度为86.4 MB/s,我们可以通过
--device-read/write-bps和--device-read/write-iops参数对其进行限制。
这里要先说一下区别:
- bps:每秒读写的数据量。
- iops:每秒IO的次数。
为了直观,这里我们直接使用BPS作为限制条件:
▼sh复制代码docker run -it --device-write-bps=/dev/sda:10MB ubuntu
看看:
可以看到现在的速度就只有10MB左右了。
容器监控
最后我们来看看如何对容器的运行状态进行实时监控,我们现在希望能够对容器的资源占用情况进行监控,该怎么办呢?
我们可以使用stats命令来进行监控:
▼sh复制代码docker stats
可以实时对容器的各项状态进行监控,包括内存使用、CPU占用、网络I/O、磁盘I/O等信息,当然如果我们限制内存的使用的话:
▼sh复制代码docker run -d -m 200M nagocoler/springboot-test:1.0
除了使用
stats命令来实时监控情况之外,还可以使用top命令来查看容器中的进程:
▼sh复制代码docker top 容器ID/名称
当然也可以携带一些参数,具体的参数与Linux中ps命令参数一致,这里就不多做介绍了。
但是这样的监控是不是太原始了一点?有没有那种网页面板可以进行实时监控和管理的呢?有的。
我们需要单独部署一个Docker网页管理面板应用,一般比较常见的有:Portainer,我们这里可以直接通过Docker镜像的方式去部署这个应用程序,搜索一下,发现最新版维护的地址为:https://hub.docker.com/r/portainer/portainer-ce
CE为免费的社区版本,当然也有BE商业版本,这里我们就直接安装社区版就行了,官方Linux安装教程:https://docs.portainer.io/start/install/server/docker/linux,包含一些安装前需要的准备。
首先我们需要创建一个数据卷供Portainer使用:
▼sh复制代码docker volume create portainer_data
接着通过官方命令安装启动:
▼sh复制代码docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest
注意这里需要开放两个端口,一个是8000端口,还有一个是9443端口。

进入后就需要我们进行注册了,这里我们只需输入两次密码即可,默认用户名就是admin,填写完成后,我们就可以开始使用了:
点击Get Started即可进入到管理页面,我们可以看到目前有一个本地的Docker服务器正在运行:
我们可以点击进入,进行详细地管理,不过唯一缺点就是没中文,挺难受的,也可以使用非官方的汉化版本:https://hub.docker.com/r/6053537/portainer-ce。
单机容器编排
最后我们来讲解一下Docker-Compose,它能够对我们的容器进行编排。比如现在我们要在一台主机上部署很多种类型的服务,包括数据库、消息队列、SpringBoot应用程序若干,或是想要搭建一个MySQL集群,这时我们就需要创建多个容器来完成来,但是我们希望能够实现一键部署,这时该怎么办呢?我们就要用到容器编排了,让多个容器按照我们自己的编排进行部署。
快速开始
在Linux环境下我们需要先安装一下插件:
▼sh复制代码sudo apt install docker-compose-plugin
这里我们就以部署SpringBoot项目为例,我们继续使用之前打包好的SpringBoot项目,现在我们希望部署这个SpringBoot项目的同时,部署一个MySQL服务器,一个Redis服务器,这时我们SpringBoot项目要运行的整个完整环境,先获取到对应的镜像:
▼sh复制代码docker pull mysql/mysql-server docker pull redis
接着,我们需要在自己的本地安装一下DockerCompose,下载地址:https://github.com/docker/compose/releases,下载自己电脑对应的版本,然后在IDEA中配置:
下载完成后,将Docker Compose可执行文件路径修改为你存放刚刚下载的可执行文件的路径,Windows直接设置路径就行,MacOS下载之后需要进行下面的操作:
▼sh复制代码mv 下载的文件名称 docker-compose sudo chmod 777 docker-compose sudo mv docker-compose /usr/local/bin
配置完成后就可以正常使用了,否则会无法运行,接着我们就可以开始在IDEA中编写docker-compose.yml文件了。
这里点击右上角的“与服务工具窗口同步”按钮,这样一会就可以在下面查看情况了。
我们现在就从头开始配置这个文件,现在我们要创建三个服务,一个是MySQL服务器,一个是Redis服务器,还有一个是SpringBoot服务器,需要三个容器来分别运行,首先我们先写上这三个服务:
▼yaml复制代码version: "3.9" #首先是版本号,别乱写,这个是和Docker版本有对应的 services: #services里面就是我们所有需要进行编排的服务了 spring: #服务名称,随便起 container_name: app_springboot #一会要创建的容器名称 mysql: container_name: app_mysql redis: container_name: app_redis
这样我们就配置好了一会要创建的三个服务和对应的容器名称,接着我们需要指定一下这些容器对应的镜像了,首先是我们的SpringBoot应用程序,可能我们后续还会对应用程序进行更新和修改,所以这里我们部署需要先由Dockerfile构建出镜像后,再进行部署:
▼yaml复制代码spring: container_name: app_springboot build: . #build表示使用构建的镜像,.表示使用当前目录下的Dockerfile进行构建
我们这里修改一下Dockerfile,将基础镜像修改为已经打包好JDK环境的镜像:
▼dockerfile复制代码FROM adoptopenjdk/openjdk8 COPY target/DockerTest-0.0.1-SNAPSHOT.jar app.jar CMD java -jar app.jar
接着是另外两个服务,另外两个服务需要使用对应的镜像来启动容器:
▼yml复制代码mysql: container_name: app_mysql image: mysql/mysql-server:latest #image表示使用对应的镜像,这里会自动从仓库下载,然后启动容器 redis: container_name: app_redis image: redis:latest
还没有结束,我们还需要将SpringBoot项目的端口进行映射,最后一个简单的docker-compose配置文件就编写完成了:
▼yaml复制代码version: "3.9" #首先是版本号,别乱写,这个是和Docker版本有对应的 services: #services里面就是我们所有需要进行编排的服务了 spring: #服务名称,随便起 container_name: app_springboot #一会要创建的容器名称 build: . ports: - "8080:8080" mysql: container_name: app_mysql image: mysql/mysql-server:latest redis: container_name: app_redis image: redis:latest
现在我们就可以直接一键部署了,我们点击下方部署按钮:
E3xul.webp" alt="image-20220703182541976" width="100%" />

可以看到,这里确实是按照我们的配置,创建了3个容器,并且都是处于运行中,可以正常访问:


这个网络默认也是使用bridge作为驱动:
这样,我们就完成了一个简单的配置,去部署我们的整套环境。
部署完整项目
前面我们学习了使用docker-compose进行简单部署,但是仅仅只是简单启动了服务,我们现在来将这些服务给连起来。首先是SpringBoot项目,我们先引入依赖:
▼xml复制代码<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
接着配置一下数据源,等等,我们怎么知道数据库的默认密码是多少呢?所以我们先配置一下MySQL服务:
▼yaml复制代码mysql: container_name: app_mysql image: mysql/mysql-server:latest environment: #这里我们通过环境变量配置MySQL的root账号和密码 MYSQL_ROOT_HOST: '%' #登陆的主机,这里直接配置为'%' MYSQL_ROOT_PASSWORD: '123456.root' #MySQL root账号的密码,别设定得太简单了 MYSQL_DATABASE: 'study' #在启动时自动创建的数据库 TZ: 'Asia/Shanghai' #时区 ports: - "3306:3306" #把端口暴露出来,当然也可以不暴露,因为默认所有容器使用的是同一个网络
有关MySQL的详细配置请查阅:https://registry.hub.docker.com/_/mysql
接着我们将数据源配置完成:
▼yaml复制代码spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://app_mysql:3306/study #地址直接输入容器名称,会自动进行解析,前面已经讲过了 username: root password: 123456.root
然后我们来写点测试的代码吧,这里我们使用JPA进行交互:
▼xml复制代码<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
▼java复制代码@Data @AllArgsConstructor @NoArgsConstructor @Entity @Table(name = "db_account") public class Account { @Column(name = "id") @Id long id; @Column(name = "name") String name; @Column(name = "password") String password; }
▼java复制代码@Repository public interface AccountRepository extends JpaRepository<Account, Long> { }
▼java复制代码@RestController public class MainController { @Resource AccountRepository repository; @RequestMapping("/") public String hello(){ return "Hello World!"; } @GetMapping("/get") public Account get(@RequestParam("id") long id){ return repository.findById(id).orElse(null); } @PostMapping("/post") public Account get(@RequestParam("id") long id, @RequestParam("name") String name, @RequestParam("password") String password){ return repository.save(new Account(id, name, password)); } }
接着我们来修改一下配置文件:
▼yaml复制代码spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://app_mysql:3306/study username: root password: 123456.root jpa: database: mysql show-sql: true hibernate: ddl-auto: update #这里自动执行DDL创建表,全程自动化,尽可能做到开箱即用
现在代码编写完成后,我们可以将项目打包了,注意执行我们下面的打包命令,不要进行测试,因为连不上数据库:
▼sh复制代码mvn package -DskipTests
重新生成jar包后,我们修改一下docker-compose配置,因为MySQL的启动速度比较慢,我们要一点时间等待其启动完成,如果连接不上数据库导致SpringBoot项目启动失败,我们就重启:
▼yaml复制代码spring: #服务名称,随便起 container_name: app_springboot #一会要创建的容器名称 build: . ports: - "8080:8080" depends_on: #这里设置一下依赖,需要等待mysql启动后才运行,但是没啥用,这个并不是等到启动完成后,而是进程建立就停止等待 - mysql restart: always #这里配置容器停止后自动重启
然后我们将之前自动构建的镜像删除,等待重新构建:
现在我们重新部署docker-compos吧:
当三个服务全部为蓝色时,就表示已经正常运行了,现在我们来测试一下吧:
接着我们来试试看向数据库传入数据:
这样,我们的项目和MySQL基本就是自动部署了。
接着我们来配置一下Redis:
▼xml复制代码<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
接着配置连接信息:
▼yaml复制代码spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://app_mysql:3306/study username: root password: 123456.root jpa: database: mysql show-sql: true hibernate: ddl-auto: update redis: host: app_redis
▼java复制代码//再加两个Redis操作进来 @Resource StringRedisTemplate template; @GetMapping("/take") public String take(@RequestParam("key") String key){ return template.opsForValue().get(key); } @PostMapping("/put") public String put(@RequestParam("key") String key, @RequestParam("value") String value){ template.opsForValue().set(key, value); return "操作成功!"; }
最后我们来配置一下docker-compose的配置文件:
▼yaml复制代码redis: container_name: app_redis image: redis:latest ports: - "6379:6379"
OK,按照之前的方式,我们重新再部署一下,然后测试:
这样我们就完成整套环境+应用程序的配置了,我们在部署整个项目时,只需要使用docker-compose配置文件进行启动即可,这样就大大方便了我们的操作,实现开箱即用。甚至我们还可以专门使用一个平台来同时对多个主机进行一次性配置,大规模快速部署。
