docker

相关概念

Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。——菜鸟教程

docker三要素

镜像

类似面向对象编程语言中类的概念,在面向对象编程语言中,通过一个模板构造一个实例,这个‘模板’类似于docker中镜像的概念。类比使用过的虚拟化软件 vmware ,镜像类似其中的iso映像文件。

容器

容器是可写的,相当于一个运行的实例。通过基础镜像进行创建。在面向对象编程语言中,通常我们需要这样去创建一个实例,这里拿java举例

1
Student stu1 = new Student()

而在docker中我们是这样创建一个容器的。

1
docker run [镜像名称/ID]

镜像仓库

镜像仓库顾名思义就是存放镜像的地方,类似代码仓库。方便镜像的管理。
相关的操作和git的那一套类似。

基础命令

描述 cmd 常用参数\说明
启动一个容器 docker run 镜像名称 -d 后台运行 -it 以交互式终端运行容器 –name给容器分配别名,不分配docker会默认分配 -p指定端口映射 -P随机端口映射
查看当前正在运行的容器 docker ps -a 列出所有的容器,包括已经停止的
查看本地存储的所有镜像 docker images
在运行的容器内执行命令 docker exec 容器名称或者ID 命令 常见的例如需要进行交互的容器:操作系统镜像
停止容器 docker stop 容器名称或ID 用于停止某个运行中的容器,可以停止多个
重新启动一个容器 docker restart 容器名称或ID 用于重启正在运行或者已经停止运行的容器
启动停止的容器 docker start 容器名称或ID 用于启动已经停止的容器
删除已经停止的容器 docker rm 容器名称或ID 用于删除已经停止的容器,如果该容器不是停止状态,执行该命令会报错,可以加上-f参数进行强制删除
删除镜像 docker rmi 镜像名称或ID 用于删除镜像
查找镜像 docker search 镜像名称 通常在docker pull之前需要查找以下远程的镜像仓库。
拉取镜像 docker pull 镜像名称 类似git pull从远程仓库拉取一个镜像
推送镜像 docker push 构件好的镜像 类似git push,将本地的镜像推送到远程仓库
查看容器运行日志 docker logs 容器名称或ID
查看容器或者镜像的详细信息 docker inspect 容器或镜像名称/ID

mysql容器实战

如果不小心删除了容器,没有对数据进行一个备份的话,将造成严重的问题。docker提供了数据卷这个东西。即使删除了容器实
例,下次再创建新的容器实例,按照备份的数据卷依然能够恢复到删除时的状态。并且配置能够复用!

1
2
3
4
5
6
7
8
9
10
11
docker run 
-d #后台运行
-p 3308:3306 #端口映射配置
--privileged=true #文件操作权限配置
-v /home/clesbit/data/mysql/log:/var/log #mysql日志数据备份卷
-v /home/clesbit/data/mysql/data:/var/lib/mysql #mysql数据备份卷
-v /home/clesbit/data/mysql/config/my.cnf:/etc/my.cnf #mysql配置文件备份卷
-e MYSQL_ROOT_PASSWORD=123456 # root密码
--name mysql-master #别名
# --network net1 # 注意!要先创建这个网络!才能使用该配置
mysql #基础镜像

图形化管理界面

通常我们不会直接在容器里面写相关的sql语句去操纵数据库,而是通过一些图形化界面自动生成sql脚本去操纵数据库,下面要用到一个数据库连接管理工具。同样是容器化过后的。

phpmyadmin

  • 拉取镜像
1
docker pull phpmyadmin
  • 创建docker网络
    mysql容器要与phpmyadmin容器产生“交互”,当然少不了建立连接,在docker当中建立一个可以沟通的网络。
1
docker network create net1

查看网络是否已经创建

1
docker network ls

这里的net1是自己定义的一个网络别名。

  • 启动phpmyadmin容器
1
2
3
4
5
6
7
docker run \
--name my-phpmyadmin \ #容器别名
-d #后台运行
--network net1 \ # 连接到net1网络
-p 8080:80 \ # 端口映射
-e PMA_HOST=mysqlpro \ # 连接到这个数据库
phpmyadmin #基础镜像名

所有东西都准备好了之后,访问http://localhost:8080 就可以看到phpadmin的登录页面

并且可以看到,之前备份的数据还是存在的,这里是我之前备份的一些数据卷

至此,环境的搭建告一段落了……

踩坑点

注意文件的权限问题

1
ls -l

有时候配置加载与数据恢复错误很大概率是文件权限的问题。不想被权限问题困扰太多建议切换到root用户。

首先要先去排查日志。通过

1
docker logs 容器名称/ID

去排查,注意镜像软件的版本问题。这里的mysql版本是9.1.0

docker网络

docker网络有四大模式,分别是bridge,host,none,container,下面简要的说明一下这四种模式。

网络模式 说明
bridge 为每一个容器分配一个ip,并且将容器连接到指定的虚拟网桥,创建docker网络的时候默认是这个模式,这个模式也是最常用的模式
host 容器不会虚拟出自己的网卡,配置自己的ip,而是和使用宿主机的ip和端口
none 容器有独立的network namespace,但是并没有进行任何的网络配置。
container 新创建的容器不会创建自己的网卡和配置自己的ip,而是和一个指定的容器共享ip、端口范围等。

bridge 模式

bridge 模式的特点

docker 服务默认会创建一个 docker0 网桥(其上有一个 docker0 内部接口),该桥接网络的名称为 docker0,他在内核层联通了其他的物理或虚拟网卡,这就将所有容器和本地主机都放到同一个物理网络docker 默认指定了 docker0 接口的 ip 地址和子网掩码,让主机和容器之间可以通过网桥互相通信。

查看宿主机的网络配置

1
ifconfig

在这里,可以看到一个 docker 服务创建的 docker0 网桥。

  • 整个宿主机的网桥模式都是 docker0,类似交换机有一堆的接口,每个接口叫 vethXXX(virtual ethernet 虚拟以太网),在本地主机和容器内分别创建一个虚拟接口,并且让他们彼此联通。
  • 每个容器实例内部也有一块网卡,每个接口叫 eth0
  • docker0 上面的每个 veth 匹配某个容器实例内部的 eth0,一一对应的关系。

这样宿主机将每一个容器实例都连接到这个内部虚拟网络上,两个容器在同一个网络上,会从网关拿到各自分配到的 ip 地址,此时这两个容器的网络是互通的。

下面我们来验证一下是不是这样的。

下面启动两台 tomcat 服务器

1
2
docker run -d -p 8081:8080 --name t1 billygoo/tomcat8-jdk8
docker run -d -p 8082:8080 --name t2 billygoo/tomcat8-jdk8

然后我们使用如下命令查看 ip 的相关信息

1
ip addr

看最后的两条

7: vethc573879@if6

9: veth90dfb51@if8

然后我们分别进入到两台容器实例的内部

1
docker exec -it container_name bash

⚠️ 在容器内部执行命令

1
ip addr

然后这个时候你就会看到

6: eth0@if7:

同理,第二台容器实例也是这样的。

查看默认启动容器的网络说明:这个小结所创建的两个容器实例没有做任何特殊的网络配置

1
docker inspect container_name | tail -n 20

可以看到,这里的网络模式为bridge模式,并且分配的ip地址是 172.17.0.2 ,网关是 172.17.0.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"MacAddress": "02:42:ac:11:00:02",
"NetworkID": "60c04bfae7e06bdee40a6a793b396ad9d0bd4a2c1a753ec3dc2889041a8bcd6c",
"EndpointID": "8a874d273c9d827a8179583463d7b5d11fd31095ce16ed9d0a319b43bd4bee88",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"DriverOpts": null,
"DNSNames": null
}

下面再查看一台容器的ip分配情况,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"MacAddress": "02:42:ac:11:00:03",
"NetworkID": "60c04bfae7e06bdee40a6a793b396ad9d0bd4a2c1a753ec3dc2889041a8bcd6c",
"EndpointID": "01b28e79d9782d7b51d195db897a3255bd6ef64ef827ad5280cb753ec8c0f13a",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.3",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"DriverOpts": null,
"DNSNames": null
}

很明显第二个容器也是bridge模式,并且分配的ip是172.17.0.3,网关一致。

查看docker默认的网络

1
docker network ls

列出所有docker网络

1
2
3
4
5
NETWORK ID     NAME      DRIVER    SCOPE
60c04bfae7e0 bridge bridge local
eebe202f70b8 host host local
681d9b1764d8 net1 bridge local
860433c5e265 none null local

这里有一个名字叫bridge的网络,下面看一下它具体的网络配置

1
docker inspect network bridge | tail -n 30

执行这条指令之后,可以看到如下的信息。可以看到,和我们之前看到的网络配置信息对应上了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
....#此处省略部分配置信息
"Containers": {
"694e84892c185f52838a1137399a0d221e30400af343bbc586d51b49ffde0d1c": {
"Name": "u1",
"EndpointID": "8a874d273c9d827a8179583463d7b5d11fd31095ce16ed9d0a319b43bd4bee88",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
},
"815f2410a24052c5af9cd6a01dd6d6a296028c663074e7fa7bf47583640ba474": {
"Name": "u2",
"EndpointID": "01b28e79d9782d7b51d195db897a3255bd6ef64ef827ad5280cb753ec8c0f13a",
"MacAddress": "02:42:ac:11:00:03",
"IPv4Address": "172.17.0.3/16",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
}

值得注意的一点是,docker默认容器实例的网络分配的ip是动态调整的,生成的ip并不会固定。比如说,当一个mysql容器实例挂掉之后,它对应的ip地址会被docker回收,然后这个时候系统后续又创建了几个容器实例,后续之前的mysql容器再启动之后,ip可能是不再是之前的ip了。eg:下面我将停止一个容器,然后又启动一个新的容器实例,然后执行一条命令。

1
docker inspect network bridge | tail -n 30

显然,之前的ip已经被占用了,这个时候被停止掉的容器再重新启动,默认分配到的ip就不会之前的ip了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"Containers": {
"237a7758b84b912094ef82625f82a6cf02028bda170ecff31357d641a41bc511": {
"Name": "u3",
"EndpointID": "8c2fe3a52a7851484681942184b6da42dc6b77f14d853b032680c3b6afd70744",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16", # 新的容器实例
"IPv6Address": ""
},
"815f2410a24052c5af9cd6a01dd6d6a296028c663074e7fa7bf47583640ba474": {
"Name": "u2",
"EndpointID": "01b28e79d9782d7b51d195db897a3255bd6ef64ef827ad5280cb753ec8c0f13a",
"MacAddress": "02:42:ac:11:00:03",
"IPv4Address": "172.17.0.3/16",
"IPv6Address": ""
}
},

host 模式

host 模式与宿主机共用网络配置。

host 模式下,指定端口不再起作用,并且允许容器实例的时候会出现一个警告:

WARNING: Published ports are discarded when using host network mode

意思是说,在使用主机网络模式时,已发布的端口将被丢弃。

1
docker run -d --network host -p 8083:8080 --name t3 billygoo/tomcat8-jdk8

从上图可以看到,容器是已经启动了。既然是共用宿主机的配置,猜测应该访问宿主机的 8080 端口即可访问到 tomcat

再次进行验证。进入容器 t3 容器的内部,查看网络配置

1
docker exec -it t3 bash

可以看到,使用ip addr指令出现的信息与宿主机是一致的。

这里就完美的验证了 host 模式的特点,与宿主机共享一份网络配置。

none 模式

当把容器实例的网络模式设置成none的时候,就相当于禁用掉了这个容器对外的网络。docker 只会给这个容器创建一个本地回环测试的 lo 网络。

1
docker run -d --network none --name t4 billygoo/tomcat8-jdk8

进入到容器中使用 ip addr指令,发现只存在一个 lo 本地回环测试网络。

container 模式

当设置一个容器的网络模式为容器模式的时候,这个容器的网络会与指定的容器共用一个 网络配置。

下面启动两个 alpine 实例进行验证

1
docker run -it --name alpine0 alpine /bin/sh
1
docker run -it --name alpine1 --network container:alpine0 alpine /bin/sh

在宿主机上查看相关的网络信息。

1
ip addr

可以看到,只虚拟出了一个 veth

15: veth9568a07@if14:

下面再进入到 alpine0 和 alpine1 里面去看一下相关的网络配置。

可以看到,两个容器实例的网络配置是一样的。

当我们关掉 alpine0 之后,alpine1 的网络是这样的。

可以看到 alpine1 里面的 14: eth0@if15也被移除了

自定义网络

在实际情况下,我们需要对网络进行管理,这样我们的服务才能更好的维护。

对于网络的自定义,之前 docker 网络有一个比较值得注意点,也就是容器实例的 <font style="color:#DF2A3F;">ip</font> 并不固定,可能这次启动时这个 ip,下一次启动可能就不是这个 ip 了。如果在我们服务里面,不同容器之间使用到了其他容器的 ip 进行容器之间的通信。很大概率后续会导致服务连接错误等状况。docker 也考虑到了这一点。所以在实际情况中,我们可以手动的创建一个网络,然后将容器实例放到我们创建的自定义网络上。(可以考虑手动分配 ip 地址),也可以让他自动分配 ip 地址。然后容器实例之间通过服务名(具体提现为容器名字)来通信,这样就不会因为 动态 <font style="color:#DF2A3F;">ip</font> 地址而出现服务错误的问题。

下面来验证一下

1
docker network create cles_net

然后我们创建两个容器,将其连接到我们新创建的 cles_net网络上cles_net是自定义的网络名称,可以自己取。

1
docker run -d --network cles_net -p 8081:8080 --name t1 billygoo/tomcat8-jdk8 
1
docker run -d --network cles_net -p 8082:8080 --name t2 billygoo/tomcat8-jdk8 

然后分别进入到这两个容器内互相 ping 对方的容器名

可以看到,完全是可以互相联通的。

⚠️如果我们没有指定容器连接上自定义网络的话。他们之前 ping 容器名是 ping 不通的。虽然他们是在同一个网段下面的。

比如我们这样启动两个容器实例

1
2
docker run -d -p 8081:8080 --name t1 billygoo/tomcat8-jdk8 
docker run -d -p 8082:8080 --name t2 billygoo/tomcat8-jdk8

然后两台容器之间检测连通性。

docker-compose

基本概念

在之前的 mysql 容器实战的时候。我们要先创建一个自定义网络net1,然后使用docker run指令去启动我们的 mysql 容器,然后再用docker run指令去 启动我们的phpadmin。这个启动顺序是不可以乱来的,也就是说要让我们的服务正常的启动并且运行,是要有一个合理的启动顺序的。

但是如果后续容器多了起来,这样操作就非常的繁琐了。并且不利于维护。docker-compose 让我们基于一个 yml 文件去管理我们一整个容器集群的启动。

安装 docker compose

docker compose 的安装官方说的已经非常详细了,并且不同的操作系统提供了不同的安装方式,之前已经安装了 docker desktop,就不需要再安装 docker compose 了,他已经集成到里面了

官方示例实战

官方示例

在做这个实验的时候,踩了很多坑,包括不限于:docker compose 的安装、容器内网络问题、容器编排完毕之后镜像拉取失败从而导致整个服务起不来……

那么就开始吧。

  1. 首先创建一个工作目录
1
2
mkdir composetest
cd composetest
  1. 然后创建一个 python 文件

这个 python 文件开启了一个很简单的 web 服务器,用的是 flask ,然后使用 redis 去记录某台主机访问这个网站的次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import time

import redis
from flask import Flask

app = Flask(__name__)
cache = redis.Redis(host='redis', port=6379)


def get_hit_count():
retries = 5
while True:
try:
return cache.incr('hits')
except redis.exceptions.ConnectionError as exc:
if retries == 0:
raise exc
retries -= 1
time.sleep(0.5)


@app.route('/')
def hello():
count = get_hit_count()
return 'Hello World! I have been seen {} times.\n'.format(count)
  1. 接下来就是把我们的服务容器化了。

编写 python 的依赖文件requirements.txt

这里只是一个示例,所以依赖不多,就一个 flask 和 redis 包

1
2
flask
redis

在这里踩了很多坑

  • alpine 容器内部拉取相关依赖的时候超时。跑了几千秒都没搞好。
  • 然后就是 pip 拉取相关的 python 依赖超时

总结,当出现这种情况的时候,停止构建,在 Dockerfile 中添加切换国内镜像源的脚本。下面文件的第五行和第八行是新增的。

1
2
3
4
5
6
7
8
9
10
11
FROM python:3.10-alpine
WORKDIR /code
ENV FLASK_APP app.py
ENV FLASK_RUN_HOST 0.0.0.0
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache gcc musl-dev linux-headers
COPY requirements.txt requirements.txt
RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
RUN pip install -r requirements.txt
COPY . .
CMD ["flask", "run"]
  1. 编写 yml 文件
1
2
3
4
5
6
7
8
# yaml 配置
services:
web:
build: .
ports:
- "5000:5000" # 暴露的端口
redis:
image: "redis:alpine" #指定redis的镜像
  1. 文件都准备好了之后,就可以开始启动 docker compose 了

在工作目录下面输入指令

1
docker compose up -d #如果是旧版本的docker compose,使用docker-compose up -d

坑点:由于本地没有python:3.10-alpine这个镜像,这个时候 docker 会到 docker hub 去拉取这个镜像,很有可能会因为网络问题导致镜像拉取失败,从而 docker compose up 失败。

解决方案,先把这个镜像拉取到本地,然后再执行 docker compose up -d 指令

不出意外的,成功的将服务跑起来了

并且访问 localhost:5000 能看到如下的界面,当刷新界面的时候,计数器就会增加。

值得注意的是,即使我们没有在任何地方指定这个服务集群的网络,docker 会为我们创建一个默认的网络,当我们使用指令

1
docker compose down #关闭服务

这个时候 docker 会删除掉这个默认网络。