Docker学习笔记


容器化技术不是模拟的一个完整的操作系统,容器内的应用直接运行在宿主机内核,自身没有自己的内核,也没有去虚拟硬件,每个容器间是互相隔离的。

当 docker 新建一个容器时,不需要像虚拟机一样重新加载一个操作系统内核,它直接用宿主机操作系统内核。

linux 基本命令

1
2
whereis [FILENAME]   # 查找文件
ip addr # 查看当前ip地址

组成

  • 镜像(image):创建容器的模板
  • 容器(container):docker 利用容器技术独立运行的一个或一组应用,是服务
  • 仓库(repository):存放镜像的地方

安装卸载

安装

参考官方文档,这里只做部分注解

1
2
# 安装前最好更新下软件包
sudo yum makecache fast

docker-ce:社区版 docker-ee:企业版

启动测试镜像

docker 执行 run 命令时,会本地不存在该镜像,会前去镜像仓库查找并拉取

1
docker run hello-world

卸载

1
2
3
4
5
# 卸载依赖
sudo yum remove docker-ce docker-ce-cli containerd.io
# 删除资源
sudo rm -rf /var/lib/docker
sudo rm -rf /var/lib/containerd

命令

官方文档

帮助命令

1
2
3
4
docker version       # docker 版本信息
docker info # docker 详细信息(系统信息、镜像和容器数量等)
docker stats # 查看当前所有运行容器状态
docker 命令 --help # 帮助

镜像命令

docker images 查看本地主机上的镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@marry ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 7614ae9453d1 3 months ago 113MB

# 字段
REPOSITORY 仓库源
TAG 版本
IMAGE ID id
CREATED 创建时间
SIZE 大小

# 常用可选项
--all , -a # 显示所有镜像
--quiet , -q # 只显示镜像id

docker search 搜索镜像

1
2
3
4
5
6
[root@marry ~]# docker search redis
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
redis Redis is an open… 10694 [OK]

# 可选项
--filter , -f # 按条件搜索,例:-f=STARS=10000

docker pull 下载镜像

默认下载的是剔除了所有不必要文件的最小镜像,保证最小的可运行环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@marry ~]# docker pull redis
Using default tag: latest # 不写tag,默认拉取最新版
latest: Pulling from library/redis
ae13dd578326: Pull complete # 分层下载,其他镜像若用到会自动共用,无需再次下载
e6f25d21ebb3: Pull complete
601cc6106ba1: Pull complete
5b8be2fd806e: Pull complete
950c3791111a: Pull complete
567b7ad78092: Pull complete
Digest: sha256:771c20f2507b0e6b59a857abc3be09bb6764bb024db35f379ada9fe62ad932c5 # 签名
Status: Downloaded newer image for redis:latest
docker.io/library/redis:latest # 真实地址,即等价于运行 docker pull docker.io/library/redis:latest

# options
:tag # 指定版本下载,例:docker pull redis:6.0.16

docker rmi 删除镜像

1
2
3
4
5
6
7
# options
# 默认不能删除存在的容器的镜像
-f # 强制删除

# examples
docker rmi $(docker images -aq) # 删除所有未使用镜像
docker rmi -f $(docker images -aq) # 删除所有镜像

查看镜像元数据

1
docker inspect [IMAGE ID]

查看镜像构建过程

1
docker history [IMAGE ID]

docker save 镜像导出

1
2
3
4
5
6
7
docker save [OPTIONS] IMAGE

# options
-o # 导出的路径

# example
[root@marry ~]# docker save -o ./centos-node.tar marrydream/centos-node

docker load 镜像导入

1
2
3
4
5
6
7
docker load [OPTIONS]

# options
-i # 导入的镜像路径

# example
[root@marry ~]# docker load -i ./centos-node.zip

容器命令

有了镜像才能创建容器

docker run 新建容器并启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
docker run [OPTIONS] IMAGE [COMMAND]

# options
-d # 后台运行
-i # 使用交互方式,常和 t 连用
-t # 指定终端,常和 i 连用
-p # 指定容器端口
-p 主机端口:容器端口 # 映射到主机的指定端口,常用
-p ip:主机端口:容器端口
-p 容器端口
容器端口
-P # 随即指定容器端口(大写)
-v # 卷挂载
-e # 环境配置
--rm # 多用于测试,用完即删
--name # 容器名字,用来区分容器

# command
/bin/bash # 指定运行程序
-c "linux 命令" # 执行linux命令

示例:创建并进入容器

交互需要获取控制台,linux 的控制台一般在 bin 目录下,这里是使用 bin 目录下的 bash 命令的意思

1
2
3
# 可以看到执行后进入了一个新的控制台页面
[root@marry ~]# docker run -it redis /bin/bash
[root@ac4a70e3bbcc /]#

退出容器

1
2
exit           # 退出即停止容器(exec方式进入容器的话退出不会停止)
Ctrl + P + Q # 退出后容器继续运行

当启动没有前台应用的容器时,如 centos ,若要使得容器启动后保持运行,且支持被 startrestart 启动容器,必须携带 -it 参数并指定交互式控制台,即 /bin/bash,否则启动即停止。
这是因为 Docker 容器若后台运行,就必须有一个前台进程(即控制台)。前台进程结束,容器就会退出。若未给容器指定前台进程且容器并未自带前台进程,容器会发现自己没有提供前台服务,就会立刻自行终止。
若想要创建后台运行的容器且不进入容器内部,再加一个 -d 参数即可。

docker ps 列举容器

1
2
3
4
5
6
# options
# 默认列举当前正在运行的容器
-a # 列举出所有容器(不管有没有正在运行)
-q # 只显示[CONTAINER ID]
-n=? # 显示最近创建的?个容器

docker rm 删除容器

1
2
3
4
5
6
7
# options
# 默认不能删除正在运行的容器
-f # 强制删除

# examples
docker rm $(docker ps -aq) # 删除所有未运行容器
docker rm -f $(docker ps -aq) # 删除所有容器

进入正在运行的容器

1
2
3
4
# 进入容器后开启一个新的命令行
docker exec -it [CONTAINER ID] /bin/bash
# 直接进入当前正在执行的命令行
docker attach [CONTAINER ID]

启动停止和重启容器

1
2
3
4
docker start [CONTAINER ID]     # 启动停止的容器
docker restart [CONTAINER ID] # 重启容器
docker stop [CONTAINER ID] # 停止启动的容器
docker kill [CONTAINER ID] # 强制停止容器

从容器内拷贝文件至主机

1
2
3
4
docker cp [CONTAINER ID]:容器文件路径 本机路径

# example
docker cp 7b44fc7ccff2:/home/test.json . # 拷贝 test.json 至本机当前目录

查看容器中的进程

1
docker top [CONTAINER ID]

其他常用命令

docker logs 查看日志

1
2
3
4
5
6
7
8
9
docker logs [OPTIONS] [CONTAINER ID]

# options
-t # 显示日志输出
-f # 显示时间戳
--tail num # 从日志末尾开始显示后 num 条

# 例
docker logs -tf --tail 100 9439778ba047 # 打印后100条日志

docker commit 提交容器成为一个新副本

1
2
3
4
5
6
7
8
9
docker commit -m 提交的描述信息 -a 作者 [CONTAINER ID] 目标镜像名:[TAG]   # 和git类似

# example
docker commit -m 可运行的tomecat -a marrydream d7d759b654c6 marrytomcat:1.0.0

[root@marry ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
marrytomcat 1.0.0 07a0b15427da 18 minutes ago 684MB
tomcat latest fb5657adc892 3 months ago 680MB

并未做出影响大小的改动,但最终大小却多了 4MB,这就牵扯到了镜像分层原理

容器数据卷

如果数据在容器里,容器删除时数据也会丢失。现在希望数据可以保存在本机内,不会跟随着容器消失。 数据卷会将 docker 内产生的数据同步到本地,即将容器内的目录挂载到本机上,且支持容器间数据共享,即多个容器共用一个目录。

可以理解成双向绑定

命令挂载 -v

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
docker run -it -v 主机目录:容器内目录   # 可以挂载多个,目录不存在会直接创建

[root@marry ~]# docker run -itd --name test-redis -v /usr/local/redis/data:/data redis /bin/bash

# 挂载后使用 docker inspect [CONTAINER ID] 查看
[root@marry ~]# docker inspect test-redis
"Mounts": [
{
"Type": "bind",
"Source": "/usr/local/redis/data", # 主机内同步目录
"Destination": "/data", # 挂载的容器内目录
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
]

docker volume 查看本地数据卷信息

1
2
3
# options
ls # 查看所有的 volume
rm [name] # 删除对应 name 的未使用 volume

匿名挂载

-v 只跟一个容器内路径,主机路径也不指定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@marry ~]# docker run -itd -v /data --name test-redis redis /bin/bash

# volume name 为这样的乱码就是匿名挂载
[root@marry ~]# docker volume ls
DRIVER VOLUME NAME
local b68311b15c89f22b446f07d71c28bf0fd84b26522fb7bd125ca033775a677023

# 挂载后使用 docker inspect 查看
[root@marry ~]# docker inspect test-redis
"Mounts": [
{
"Type": "volume",
"Name": "3c6726608d8d8c74f77e23edda11379b1f91f47e24f8a644e5b9862ecb74dcc3",
"Source": "/var/lib/docker/volumes/3c6726608d8d8c74f77e23edda11379b1f91f47e24f8a644e5b9862ecb74dcc3/_data",
"Destination": "/data",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],

具名挂载

-v 卷名:容器内路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 命名为 test-name-redis
[root@marry ~]# docker run -itd -v test-name-redis:/data --name test-redis redis /bin/bash

[root@marry ~]# docker volume ls
DRIVER VOLUME NAME
local test-name-redis

# 挂载后使用 docker inspect 查看
[root@marry ~]# docker inspect test-redis
"Mounts": [
{
"Type": "volume",
"Name": "test-name-redis",
"Source": "/var/lib/docker/volumes/test-name-redis/_data",
"Destination": "/data",
"Driver": "local",
"Mode": "z",
"RW": true,
"Propagation": ""
}
],

所有 dockers 容器内未指定主机路径的数据卷,都是在 /var/lib/docker/volumes/${卷名}/_data 目录下
大多数情况下都使用具名挂载,不建议使用匿名挂载

容器挂载权限

-v xxx:xxx:ro
ro:只读 rw:读写

1
2
3
4
5
6
7
# 创建只读数据卷
[root@marry ~]# docker run -itd -v /usr/local/redis/data:/data:ro --name test-redis redis /bin/bash
# 进入容器
[root@marry ~]# docker exec -it test-redis /bin/bash
# 尝试新建文件
root@083a797f4799:/data# touch test.json
touch: cannot touch 'test.json': Read-only file system

对于只读数据卷,只允许在主机这边修改其内容,容器内将无法修改卷内容

Dockerfile

官方文档官方规范

即用来构建 docker 镜像的构建文件,可以理解成命令脚本,执行即会创建一个镜像。
Dockerfile 由多个命令组成,每个命令都是一层。
由于官方镜像大多都是安装了少数命令的基础包,因此常需要通过 Dockerfile 搭建自己的镜像。

Dockerfile 内容

格式:指令(大写) 参数

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
FROM         # 这个镜像基于哪个基础镜像,最基础镜像:scratch
LABEL # 镜像信息,指定version、desc等 <key>=<value>
ENV # 构建时设置环境变量 <key>=<value>
RUN # 镜像构建时运行的 cmd 命令
ENTRYPOINT # 容器启动时运行的 cmd 命令
CMD # 容器启动时运行的 cmd 命令,会被 run image 时附加的 cmd 覆盖
ADD # 添加内容,会自动解压
COPY # 类似 ADD, 将文件拷贝到镜像中
WORKDIR # 镜像的工作目录
VOLUME # 设置卷 / 挂载的目录,生成的镜像会自动挂载设置的数据卷目录
EXPOSE # 指定暴露的端口
ONBUILD # 构建一个被继承的 DockerFile 时运行的 cmd 命令

# example
FROM centos:centos7
LABEL author="MarryDream"
LABEL version="1.0.0"
ENV MYPATH="/user/local"
WORKDIR ${MYPATH}
COPY ./fonts /usr/share/fonts/ttf-dejavu # 拷贝字体
VOLUME ["volume1", "volume2"] # 匿名挂载

# 安装 vim、ipconfig 等包,设置时区为上海
RUN yum install -y \
vim \
net-tools \
&& ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

EXPOSE 80
CMD nohup sh -c "echo ${MYPATH}"

docker build 构建

1
2
3
4
5
6
7
docker build -f [Dockerfile路径] -t [新镜像名称(小写)] [放置目录]

# example
[root@marry ~]# docker build -f ./Dockerfile -t marrydream/centos:1.0.0 .
[root@marry ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
marrydream/centos 1.0.0 1705058b7b6e 1 minutes ago 443MB

若 Dockerfile 文件名为 Dockerfile,则不需要加 -f,docker 会自动寻找当前目录下该文件名的文件

使用构建出来的镜像创建容器时,要么使用 镜像 id 构建,要么必须使用 名字:tag(如果有 tag) 来构建

–volume-from 数据卷共享

一般用于多个 mysql 或 redis 实现数据共享

1
2
3
4
5
6
7
8
9
10
11
# 使用上面的 Dockerfile 创建的镜像构建容器
[root@marry ~]# docker run -itd --name centos01 marrydream/centos:1.0.0 /bin/bash
[root@marry ~]# docker run -itd --name centos02 --volumes-from centos01 marrydream/centos:1.0.0 /bin/bash
[root@marry ~]# docker run -itd --name centos03 --volumes-from centos01 marrydream/centos:1.0.0 /bin/bash

# 直接使用 docker-hub 镜像构建数据
[root@marry ~]# docker run -itd --name redis01 -v /usr/local/redis/data:/data redis /bin/bash
[root@marry ~]# docker run -itd --name redis02 --volumes-from redis01 redis /bin/bash
[root@marry ~]# docker run -itd --name redis03 --volumes-from redis01 redis /bin/bash

# nginx01、02、03之间的数据卷内数据共享,redis01、02、03之间的同理

镜像发布

登录/退出登录 DockerHub

1
2
3
4
5
6
7
[root@marry ~]# docker login [options] [value]

# options 不必填
-u, --username # 用户名
-p, --password # 密码

[root@marry ~]# docker logout

发布镜像

1
2
3
4
docker push [author]/[ImageName]:[version]

# example
[root@marry ~]# docker push marrydream/centos-node:1.0.0

为了防止重名,最好以 作者名/镜像名 的方式构建镜像并上传
若未注明 version,则会以 latest 标签上传

阿里云容器镜像服务

  • 创建命名空间
  • 创建仓库,按教程推送
  • 务必设置镜像为教程中指定的格式,否则阿里云会拒绝 push

原理

镜像文件系统

linux 实际由一层一层文件系统组成。最底层的有 bootfs (引导系统+内核)和 rootfs (操作系统发行版)。

rootfs 即各种 linux 操作系统的发行版,包含了典型 linux 系统中的 /bin /etc /dev 等文件夹。对于一个精简的 os,rootfs 可以非常精简,只包含最基本的命令和工具。

而 docker 的镜像因为底层直接使用本机的内核,不需要携带最大的 bootfs,仅携带精简版的 rootfs 即可。因此安装一个 docker 的 centos 镜像只有 200+MB 大小。

镜像分层原理

镜像都是只读的,当容器启动时,一个新的可写层会被加到镜像顶部。这一层就是通常说的容器层,容器层之下的为只读的镜像层

新做的所有改动都是在容器层实现的,而改动之后重新打为一个新的镜像发布时,会把容器层镜像层打包为一个新的整体的镜像。

docker 网络

实践操作

ip addr 查看网卡,得到结果如下图:

主机网卡

图中有三个网卡,分别是:

  • lo: 本机地址
  • eth0: 阿里云地址
  • docker0: docker 地址

尝试 docker 和容器的连接

新运行一个容器,在内部执行 ip addr 查看:

容器内网卡

存在两个网卡,分别是 loeth0@if315,其中 eth0@if315 就是 docker 分配给容器的网络(@后的数字不固定)

若提示 ip: command not found 则需执行 yum install iproute iproute-doc

尝试在主机 ping 一下这个地址,可以 ping 通:

1
2
3
4
5
6
[root@marry ~]# ping 172.17.0.3
PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data.
64 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.045 ms
64 bytes from 172.17.0.3: icmp_seq=2 ttl=64 time=0.045 ms
64 bytes from 172.17.0.3: icmp_seq=3 ttl=64 time=0.044 ms
64 bytes from 172.17.0.3: icmp_seq=4 ttl=64 time=0.045 ms

可以看到,本机中 docker 的 ip 地址为 127.17.0.1,分配的容器的 ip 地址为 127.17.0.3。可以看出,docker 类似一个路由器,docker 和 它创建的容器均处于同一个网段。

此时在主机下再次执行 ip addr,发现多了一个 315: veth3742c50@if314 与容器内的 314: eth0@if315 相对应:

新出现的 315

尝试容器间的连接

再次用相同镜像运行一个容器,使用 ip addr 查看发现其 ip 为 127.17.0.4,网卡为 316: eth0@if317,紧接上一个容器的 314: eth0@if315:

容器2的ip地址

尝试用该容器去 ping 上一个容器,成功 ping 通:

1
2
3
4
5
6
[root@2ffe04cbda1c marry-api]# ping 172.17.0.3
PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data.
64 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.078 ms
64 bytes from 172.17.0.3: icmp_seq=2 ttl=64 time=0.057 ms
64 bytes from 172.17.0.3: icmp_seq=3 ttl=64 time=0.054 ms
64 bytes from 172.17.0.3: icmp_seq=4 ttl=64 time=0.057 ms

大白话理解

安装 docker 就是在家(主机)里装了个路由器,docker 就是那个路由器
docker 创建的每个容器就是家里连接路由器的设备
那么一个局域网内的两台设备能够连接不是很正常的事情

原理

  • 每启动一个 docker 容器,docker 都会为其分配一个 ip
  • 只要安装了 docker,就会有一个网卡 docker0
  • 使用桥接模式,evth-pair 技术,即 一对虚拟设备接口,一端连协议,一端互相连接,类似桥梁的作用
  • 所有容器在不指定网络时,都是使用的 docker0 网卡,docker 会给容器分配一个默认的可用 ip
  • docker 中所有的网络接口都是虚拟的,因为虚拟的转发效率高

补充:127.17.0.1/16 释义

这里后面的 8/16/20子网掩码的缩写形式,意思是位数,计算机中 8 为 1 位,而 ip 地址 255.255.255.255 的二进制为 00000000.00000000.00000000.00000000

因此 /8 指的该网段(即的不可分配的部分)为前 8 位,即 后面三个 255 均可以分配出去,有 256 ^ 3 种分配方式 同理 /16 为前 16 位,后面两个 255 为可分配部分,有 256 ^ 2
种分配方式 而 /20 为前 20 位,可分配部分为 第三个 255 中的 16(2 ^ 4) 个网段 加上最后一个 255,有 16 * 256 种分配方式

在 docker0 的 ip 这里的子网掩码为 /16,因此说明 docker 最多可分配 256 ^ 2 即 65536 个网络接口。

自定义网络

使用 docker network ls 查看所有网络

1
2
3
4
5
6
[root@iZ8vbe901lz5iyyekmlg7kZ adachi]# docker network ls
NETWORK ID NAME DRIVER SCOPE
0c2e0ac1664b adachi_default bridge local
15bfdafb40fb bridge bridge local
ab0360055889 host host local
f94914fdc617 none null local

网络模式:

  • bridge: 桥接模式,即在 docker 上搭桥,让新容器 0.2 0.3 通过 0.1 的 docker 访问
  • none: 不配置网络
  • host: 主机模式,和宿主机共享网络
  • container: 容器内网络连通(了解即可)

自定义网络一般使用 bridge 桥接模式

在此之前,直接 docker run 启动的容器,其实默认带着一个 --net bridge 的 option,即 docker0。
但 docker0 是不能使用容器名访问的,只能使用 ip,因此才有了 自定义网络

创建网络

1
2
3
4
5
6
7
8
9
10
11
12
docker network create [options] [name]

# options
--driver # 网络模式,不写默认为 --driver bridge
--subnet # 子网掩码
--gateway # 网关 (从哪里出去)

# example (家里路由器其实默认就是这个配置)
[root@marry ~]# docker network create --subnet 192.168.0.0/16 --gateway 192.168.0.1 marrynet

# 使用自建网络创建容器
[root@marry ~]# docker run -d --name net-test --net marrynet marrydream/centos-node /bin/bash

多个使用 自定义网络 创建的容器,互相之间可以直接使用 容器名 访问
通过自定义网络的方式,可以让各个集群各自使用不同的网络

查看网络元信息

1
2
3
4
docker network inspect  [network name]

# example
[root@marry ~]# docker network inspect marrynet

网络连通

不同网段是无法相互连接的,但容器可以和网络打通,现需要让 网络a的容器a 连接到网络b上

1
2
3
4
5
6
7
docker network connect [OPTIONS] NETWORK CONTAINER

# example
[root@marry ~]# docker network connect marrynet docker0-container

# 查询 marrynet
[root@marry ~]# docker network inspect marrynet

发现结果是 marrynet 的 container 属性中直接被追加了容器 docker0-container,即一个容器两个 ip 地址
公网 ip 和私网 ip 也是同理其实

这样处理过后,就可以实现网络 a 的容器 a ping 通网络 b 的容器 a

docker-compose

docker-compose 是 docker 官方的开源项目,用于定义、运行多个容器,根据编写的配置文件来实现批量容器编排。

安装 docker-compose

在 windows 和 macos 上,安装 docker desktop 会自动附带 docker-compose,无需手动安装,这里讲 linux 的安装方法

前往 https://github.com/docker/compose/releases 查找指定版本 release 包,现在最新版为 v2.5.0

方式一

直接执行以下指令,缺点,网速可能极慢(

1
sudo curl -L "https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

方式二

手动下载对应 release 包,自己系统环境可以通过 uname -suname -m 来查看,我这里为 linux-x86_64

下载后通过 ftp 工具上传至服务器 usr/local/bin/ 目录下,执行命令

1
2
3
4
5
6
7
8
# 设置可执行权限
sudo chmod +x /usr/local/bin/docker-compose

# 创建软连接
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

# 验证是否成功
docker-compose --version

使用步骤

  • 需要一个 Dockerfile 来保证项目在任何地方都可以运行
  • 需要一个 docker-compose.yml 来定义服务
  • 使用 docker-compose up 运行服务

docker-compose.yml

docker-compose.yml 分为三层

1
2
3
4
5
6
7
8
9
version: # 第一层:指定 docker-compose 版本
services: # 第二层:服务
service1:
# 服务配置(images等)
service2:
# 第三层:其他配置
volumes:
networks:
configs:

部分配置说明

1
2
3
4
# services
depends_on # 服务依赖关系,当前服务会在包裹的服务之后启动
- service1
- service2

docker-compose不指定容器名的情况下启动的默认容器名为:文件夹名-服务名-num