技术栈 —— Docker

欢迎关注:www.kumao.cool
Docker官网地址:https://www.docker.com
Docker文档

一、简介

Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。

一个完整的Docker有以下几个部分组成:

  • Docker Client客户端
  • Docker Daemon守护进程
  • Docker Image镜像
  • Docker Container容器
这是Docker的架构图

技术栈 —— Docker_第1张图片

简单介绍一下其中的三个概念:
  1. 镜像(Image):Docker镜像,就相当于是一个root文件系统。比如官方镜像ubuntu:16.04就包含了完整的一套 ubuntu16.04最小系统的root文件系统。
  2. 容器(Container):镜像和容器的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
  3. 仓库(Repository):仓库可看成一个代码控制中心,用来保存镜像。
Docker的工作流:
  1. 启动docker
  2. 下载镜像到本地
  3. 启动docker容器实例

二、使用Docker

1 安装docker(ubuntu系统为例)
1.1 如果系统中安装过旧版的docker,先卸载掉
sudo apt-get remove docker docker-engine docker.io containerd runc
1.2 安装docker前做一些准备工作
# 更新apt包索引,安装允许apt通过HTTPS访问仓库的包

sudo apt-get update

sudo apt-get install \
    ca-certificates \
    curl \
    gnupg \
    lsb-release
# 添加Docker的官方GPG密钥

sudo mkdir -p /etc/apt/keyrings

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# 设置镜像仓库

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
1.3 安装docker引擎
sudo apt-get update

安装最新版的docker引擎,如果要安装指定版本,参考 https://docs.docker.com/engine/install/ubuntu/

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
moon@kumao:~$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
Reading package lists... Done
Building dependency tree       
Reading state information... Done
Suggested packages:
  aufs-tools cgroupfs-mount | cgroup-lite
The following NEW packages will be installed:
  containerd.io docker-ce docker-ce-cli docker-compose-plugin
0 upgraded, 4 newly installed, 0 to remove and 214 not upgraded.
Need to get 0 B/99.2 MB of archives.
# ...
Processing triggers for man-db (2.8.3-2ubuntu0.1) ...
Processing triggers for ureadahead (0.100.0-21) ...
moon@kumao:~$ 

验证安装是否成功

sudo docker run hello-world
moon@kumao:~$ sudo docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete 
Digest: sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.
# ...
For more examples and ideas, visit:
 https://docs.docker.com/get-started/

moon@kumao:~$ 

Docker官方命令链接

2 Docker的基本命令
systemctl status docker.service            # 查看docker服务的运行状态
sudo systemctl stop docker.socket          # 停止docker socket
sudo systemctl stop docker.service         # 停止docker服务
sudo systemctl start docker.service.       # 启动docker服务
sudo systemctl restart docker.service      # 重启docker服务


moon@kumao:~$ systemctl status docker.service 
● docker.service - Docker Application Container Engine
   Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
   Active: active (running) since Wed 2022-12-07 09:49:14 CST; 3s ago
     Docs: https://docs.docker.com
 Main PID: 31131 (dockerd)
    Tasks: 7
   CGroup: /system.slice/docker.service
           └─31131 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
docker --help                # docker帮助命令
docker COMMAND --help        # docker命令帮助命令
docker --version             # 查看docker版本信息
docker info                  # 查看docker系统信息(客户端&服务端状态,镜像&容器的数量等)
3 Docker镜像操作
docker images                           # 查看本地所有的镜像
docker search 镜像名                     # 从仓库中搜索镜像
docker pull 镜像名[TAG]                  # 下载镜像(如果不写tag,默认是latest)
docker rmi 镜像名[TAG]                   # 删除镜像docker rmi -f $(docker images -aq)删除全部镜像
docker tag 镜像名:TAG 新镜像名:TAG        # 复制出一个修改了名称的镜像
docker load -i /xxx/xxx.tar             # 通过tar包导入镜像
docker save -o 镜像名 /xxx/xxx.tar       # 保存一个镜像为一个tar包
root@kumao:/home/moon# docker pull redis:7.0.5
7.0.5: Pulling from library/redis
025c56f98b67: Pull complete 
060e65aed679: Pull complete 
b95291e865b7: Pull complete 
7b879d654837: Pull complete 
4538783c407f: Pull complete 
ec5078f7c4e4: Pull complete 
Digest: sha256:dfeb5451fce377ab47c5bb6b6826592eea534279354bbfc3890c0b5e9b57c763
Status: Downloaded newer image for redis:7.0.5
docker.io/library/redis:7.0.5
root@kumao:/home/moon# docker images
REPOSITORY    TAG       IMAGE ID       CREATED         SIZE
redis         7.0.5     3e12e2ceb68f   20 hours ago    117MB
hello-world   latest    feb5d9fea6a5   14 months ago   13.3kB
root@kumao:/home/moon# 
moon@kumao:~/docker$ sudo docker save hello_world:1.0 -o ./hello_world_1.0.tar
moon@kumao:~/docker$ ls
hello_world_1.0.tar
moon@kumao:~/docker$ sudo docker load -i ./hello_world_1.0.tar 
Loaded image: hello_world:1.0
docker history 镜像名称                      # 查看docker镜像的变更历史
docker image inspect 镜像名称:标签            # 查看镜像元数据


root@kumao:/home/moon/docker/redis_data# docker history redis:7.0.5
IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
3e12e2ceb68f   23 hours ago   /bin/sh -c #(nop)  CMD ["redis-server"]         0B        
<missing>      23 hours ago   /bin/sh -c #(nop)  EXPOSE 6379                  0B        
<missing>      23 hours ago   /bin/sh -c #(nop)  ENTRYPOINT ["docker-entry…   0B        
<missing>      23 hours ago   /bin/sh -c #(nop) COPY file:e873a0e3c13001b5…   661B      
<missing>      23 hours ago   /bin/sh -c #(nop) WORKDIR /data                 0B        
<missing>      23 hours ago   /bin/sh -c #(nop)  VOLUME [/data]               0B        
<missing>      23 hours ago   /bin/sh -c mkdir /data && chown redis:redis …   0B        
<missing>      23 hours ago   /bin/sh -c set -eux;   savedAptMark="$(apt-m…   32MB      
      23 hours ago   /bin/sh -c #(nop)  ENV REDIS_DOWNLOAD_SHA=67…   0B        
      23 hours ago   /bin/sh -c #(nop)  ENV REDIS_DOWNLOAD_URL=ht…   0B        
      23 hours ago   /bin/sh -c #(nop)  ENV REDIS_VERSION=7.0.5      0B        
      23 hours ago   /bin/sh -c set -eux;  savedAptMark="$(apt-ma…   4.13MB    
<missing>      23 hours ago   /bin/sh -c #(nop)  ENV GOSU_VERSION=1.14        0B        
<missing>      23 hours ago   /bin/sh -c groupadd -r -g 999 redis && usera…   329kB     
<missing>      28 hours ago   /bin/sh -c #(nop)  CMD ["bash"]                 0B        
<missing>      28 hours ago   /bin/sh -c #(nop) ADD file:1f1efd56601ebc26a…   80.5MB 
4 Docker容器操作
docker run [可选参数] image 命令                         # 启动容器(无镜像会先下载镜像)
	--name = "Name"   容器名字
	-c   后面跟待完成的命令
	-d   以后台方式运行并且返回ID,启动守护进程式容器
	-i   使用交互方式运行容器,通常与t同时使用
	-t   为容器重新分配一个伪输入终端。也即启动交互式容器
	-p   指定容器端口    -p 容器端口:物理机端口  映射端口
	-P   随机指定端口
	-v   给容器挂载存储卷

root@kumao:/home/moon/docker# docker run -it --name="docker_redis" redis:7.0.5 /bin/bash
root@47dc1c3f847f:/data# ls
root@47dc1c3f847f:/data# 
exit                         # 直接退出容器 
crlt + p + q                 # 退出容器但是不终止运行

docker ps -a                 # 列出所有容器(不加-a就是在运行的)
docker stop 容器ID            # 停止正在运行的容器
docker start 容器ID           # 启动容器
docker restart 容器ID         # 重启容器
docker kill  容器ID           # 杀掉容器

root@kumao:/home/moon/docker# docker ps
CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS          PORTS      NAMES
fbe8daae9e9d   redis:7.0.5   "docker-entrypoint.s…"   31 seconds ago   Up 30 seconds   6379/tcp   docker_redis02
root@kumao:/home/moon/docker# docker stop fbe8daae9e9d
fbe8daae9e9d
root@kumao:/home/moon/docker# docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
root@kumao:/home/moon/docker# docker start fbe8daae9e9d
fbe8daae9e9d
root@kumao:/home/moon/docker# docker ps
CONTAINER ID   IMAGE         COMMAND                  CREATED              STATUS         PORTS      NAMES
fbe8daae9e9d   redis:7.0.5   "docker-entrypoint.s…"   About a minute ago   Up 2 seconds   6379/tcp   docker_redis02
root@kumao:/home/moon/docker# docker restart fbe8daae9e9d
fbe8daae9e9d
root@kumao:/home/moon/docker# docker ps
CONTAINER ID   IMAGE         COMMAND                  CREATED              STATUS         PORTS      NAMES
fbe8daae9e9d   redis:7.0.5   "docker-entrypoint.s…"   About a minute ago   Up 3 seconds   6379/tcp   docker_redis02
root@kumao:/home/moon/docker# docker kill fbe8daae9e9d
fbe8daae9e9d
root@kumao:/home/moon/docker# docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
docker attach 容器ID
docker exec 容器ID  
# 这两个同为 进入正在运行的容器 的命令,不同的是attach连接终止会让容器退出后台运行,而exec不会。
# docker attach是进入正在执行的终端,不会启动新的终端;而docker exec则会开启一个新的终端,可以在里面操作

root@kumao:/home/moon/docker# docker attach 47dc1c3f847f
root@47dc1c3f847f:/data# ls
root@47dc1c3f847f:/data# pwd
/data
root@47dc1c3f847f:/data# lsread escape sequence
root@kumao:/home/moon/docker# docker ps
CONTAINER ID   IMAGE         COMMAND                  CREATED             STATUS         PORTS      NAMES
47dc1c3f847f   redis:7.0.5   "docker-entrypoint.s…"   About an hour ago   Up 3 minutes   6379/tcp   docker_redis
root@kumao:/home/moon/docker# docker exec -it 47dc1c3f847f /bin/bash
root@47dc1c3f847f:/data#
docker stats 容器ID            # 查看容器状态(CPU和内存占用情况等)
docker logs 容器ID             # 查看容器日志
docker top 容器ID              # 查看容器内进程
docker rename 旧名字 新名字     # 给容器重新命名
docker rm 容器ID               # 删除容器(正在运行容器不能删除,除非加-f选项)


root@kumao:/home/moon# docker ps
CONTAINER ID   IMAGE         COMMAND                  CREATED       STATUS          PORTS      NAMES
47dc1c3f847f   redis:7.0.5   "docker-entrypoint.s…"   2 hours ago   Up 18 minutes   6379/tcp   docker_redis01
root@kumao:/home/moon# docker top 47dc1c3f847f
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                5664                5638                0                   12:20               pts/0               00:00:00            /bin/bash
root                7111                5638                0                   12:24               ?                   00:00:00            /bin/bash
root@kumao:/home/moon# 
docker cp 容器ID:容器内路径 目的主机路径       # 从容器内拷贝文件到主机(容器外同步到容器内使用挂载的方式)

root@kumao:/home/moon/docker# docker cp 47dc1c3f847f:/data/container_inner_file.txt ./
root@kumao:/home/moon/docker# ls
container_inner_file.txt  hello_world_1.0.tar

创建容器时挂载数据卷

docker run -p 宿主机端口:容器端口 -it --name 容器名 -v 宿主机目录:容器目录 镜像 命令  # 具名挂载
docker run -p 宿主机端口:容器端口 -it --name 容器名 -v 容器目录 镜像 命令           # 匿名挂载
docker run -d -P --name nginx01 -v specific-nginx:/etc/nginx:ro nginx  # 容器内只读
docker run -d -P --name nginx02 -v specific-nginx:/etc/nginx:rw nginx  # 容器内可读可写

docker inspect 容器ID         # 查看容器元信息
docker volume ls             # 查看docker数据卷列表
docker inspect VOLUME_NAME   # 查看数据卷信息


root@kumao:/home/moon/docker/redis_data# docker run -p 9999:6379 -it --name "redis_container03" -v /home/moon/docker/redis_data:/data redis:5.0.7 /bin/bash
Unable to find image 'redis:5.0.7' locally
5.0.7: Pulling from library/redis
68ced04f60ab: Pull complete 
7ecc253967df: Pull complete 
765957bf98d4: Pull complete 
52f16772e1ca: Pull complete 
2e43ba99c3f3: Pull complete 
d95576c71392: Pull complete 
Digest: sha256:938ee5bfba605cc85f9f52ff95024e9a24cf5511ba6f1cbc68ec9d91a0432125
Status: Downloaded newer image for redis:5.0.7
root@d52624399bbb:/data# ls
container_outer_file.txt
root@d52624399bbb:/data# 
root@kumao:/home/moon/docker/redis_data# docker inspect d52624399bbb
[
    {
        "Id": "d52624399bbbc15d0a0a965b9867576cd70d73ebc6102b5d6a48c51efb94dc1e",
       ......
        "Mounts": [  # 容器挂载信息
            {
                "Type": "bind",
                "Source": "/home/moon/docker/redis_data",
                "Destination": "/data",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            }
        ],
        ......
    }
]

通过容器创建新的镜像

docker commit [OPTIONS] 容器ID [REPOSITORY[:TAG]]   # 通过修改过的容器创建一个新的镜像
	-a 提交的镜像作者
	-c 应用Dockerfile指令来创建镜像
	-m 提交时的说明文字
	-p 提交时暂停容器运行


root@kumao:/home/moon/docker/redis_data# docker commit -a 'moon' -m '测试通过容器创建一个新的redis镜像' -p 47dc1c3f847f my_redis:1.0
sha256:efe0f631bd658add27c30dd34442894d4559b118c31e4ffb7c01fe9a8e7dbfd4
root@kumao:/home/moon/docker/redis_data# docker images
REPOSITORY    TAG       IMAGE ID       CREATED         SIZE
my_redis      1.0       efe0f631bd65   4 seconds ago   117MB
redis         7.0.5     3e12e2ceb68f   23 hours ago    117MB
hello_world   1.0       feb5d9fea6a5   14 months ago   13.3kB
hello-world   latest    feb5d9fea6a5   14 months ago   13.3kB
redis         5.0.7     7eed8df88d3b   2 years ago     98.2MB
5 通过Dockerfile构建镜像

技术栈 —— Docker_第2张图片

5.1 Dockerfile常用指令:

技术栈 —— Docker_第3张图片

  • FROM

设置要制作的镜像基于哪个镜像,FROM指令必须是整个Dockerfile的第一个指令,如果指定的镜像不存在默认会自动从Docker Hub上下载

  • MAINTAINER

镜像作者的信息,比如名字或邮箱地址

  • RUN

构建镜像时运行的shell命令

  • CMD

CMD指令中指定的命令会在镜像运行时执行,在Dockerfile中只能存在一个,如果使用了多个CMD指令,则只有最后一个CMD指令有效。当出现ENTRYPOINT指令时,CMD中定义的内容会作为ENTRYPOINT指令的默认参数,也就是说可以使用CMD指令给ENTRYPOINT传递参数

  • EXPOSE

声明容器的服务端口

  • ADD

拷贝文件或目录到镜像容器内,如果是URL或压缩包会自动下载或自动解压

  • COPY

拷贝文件或目录到镜像容器内,跟ADD类似,但不具备自动下载或解压功能

  • ENV

设置容器的环境变量

  • ENTRYPOINT

(1)ENTRYPOINT指令中指定的命令会在镜像运行时执行,在Dockerfile中只能存在一个,如果使用了多个ENTRYPOINT指令,则只有最后一个指令有效。
(2)ENTRYPOINT指令中指定的命令(exec执行的方式)可以通过docker run来传递参数,例如docker run images -l启动的容器将会把-l参数传递给ENTRYPOINT指令定义的命令并会覆盖CMD指令中定义的默认参数(如果有的话),但不会覆盖该指令定义的参数,例如ENTRYPOINT [“ls”,“-a”],CMD [“/etc”],当通过docker run image启动容器时该容器会运行ls -a /etc命令,当使用docker run image -l启动时该容器会运行ls -a -l命令,-l参数会覆盖CMD指令中定义的/etc参数

  • VOLUME

VOLUME指令用来设置一个挂载点,可以用来让其他容器挂载以实现数据共享或对容器数据的备份、恢复或迁移

  • USER

USER指令用于设置用户或uid来运行生成的镜像和执行RUN,CMD和ENTYRYPOINT执行命令

  • WORKDIR

WORKDIR指令用于设置Dockerfile中的RUN、CMD和ENTRYPOINT指令执行命令的工作目录(默认为/目录),该指令在Dockerfile文件中可以出现多次,如果使用相对路径则为相对于WORKDIR上一次的值,例如WORKDIR /data,WORKDIR logs,RUN pwd最终输出的当前目录是/data/logs

  • ONBUILD

ONBUILD指令用来设置一些触发的指令,用于在当该镜像被作为基础镜像来创建其他镜像时(也就是Dockerfile中的FROM为当前镜像时)执行一些操作,ONBUILD中定义的指令会在用于生成其他镜像的Dockerfile文件的FROM指令之后被执行,上述介绍的任何一个指令都可以用于ONBUILD指令,可以用来执行一些因为环境而变化的操作,使镜像更加通用

5.2 分析一个Dockerfile

redis官方镜像的Dockerfile

FROM debian:bullseye-slim  # 基于debian:bullseye-slim镜像


RUN groupadd -r -g 999 redis && useradd -r -g redis -u 999 redis  # 执行shell命令添加用户

ENV GOSU_VERSION 1.14  # 设置环境变量GOSU_VERSION
# 执行了一大段shell脚本
RUN set -eux; \
	savedAptMark="$(apt-mark showmanual)"; \
	apt-get update; \
	apt-get install -y --no-install-recommends ca-certificates dirmngr gnupg wget; \
	rm -rf /var/lib/apt/lists/*; \
	dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
	wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
	wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
	export GNUPGHOME="$(mktemp -d)"; \
	gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
	gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
	gpgconf --kill all; \
	rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
	apt-mark auto '.*' > /dev/null; \
	[ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \
	apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
	chmod +x /usr/local/bin/gosu; \
	gosu --version; \
	gosu nobody true

ENV REDIS_VERSION 7.0.5  # 设置环境变量
ENV REDIS_DOWNLOAD_URL http://download.redis.io/releases/redis-7.0.5.tar.gz
ENV REDIS_DOWNLOAD_SHA 67054cc37b58c125df93bd78000261ec0ef4436a26b40f38262c780e56315cc3

RUN set -eux; \
	\
	savedAptMark="$(apt-mark showmanual)"; \
	apt-get update; \
	apt-get install -y --no-install-recommends \
		ca-certificates \
		wget \
		\
		dpkg-dev \
		gcc \
		libc6-dev \
		libssl-dev \
		make \
	; \
	rm -rf /var/lib/apt/lists/*; \
	\
	wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL"; \
	echo "$REDIS_DOWNLOAD_SHA *redis.tar.gz" | sha256sum -c -; \
	mkdir -p /usr/src/redis; \
	tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1; \
	rm redis.tar.gz; \
	\
# disable Redis protected mode [1] as it is unnecessary in context of Docker
# (ports are not automatically exposed when running inside Docker, but rather explicitly by specifying -p / -P)
# [1]: https://github.com/redis/redis/commit/edd4d555df57dc84265fdfb4ef59a4678832f6da
	grep -E '^ *createBoolConfig[(]"protected-mode",.*, *1 *,.*[)],$' /usr/src/redis/src/config.c; \
	sed -ri 's!^( *createBoolConfig[(]"protected-mode",.*, *)1( *,.*[)],)$!\10\2!' /usr/src/redis/src/config.c; \
	grep -E '^ *createBoolConfig[(]"protected-mode",.*, *0 *,.*[)],$' /usr/src/redis/src/config.c; \
# for future reference, we modify this directly in the source instead of just supplying a default configuration flag because apparently "if you specify any argument to redis-server, [it assumes] you are going to specify everything"
# see also https://github.com/docker-library/redis/issues/4#issuecomment-50780840
# (more exactly, this makes sure the default behavior of "save on SIGTERM" stays functional by default)
	\
# https://github.com/jemalloc/jemalloc/issues/467 -- we need to patch the "./configure" for the bundled jemalloc to match how Debian compiles, for compatibility
# (also, we do cross-builds, so we need to embed the appropriate "--build=xxx" values to that "./configure" invocation)
	gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \
	extraJemallocConfigureFlags="--build=$gnuArch"; \
# https://salsa.debian.org/debian/jemalloc/-/blob/c0a88c37a551be7d12e4863435365c9a6a51525f/debian/rules#L8-23
	dpkgArch="$(dpkg --print-architecture)"; \
	case "${dpkgArch##*-}" in \
		amd64 | i386 | x32) extraJemallocConfigureFlags="$extraJemallocConfigureFlags --with-lg-page=12" ;; \
		*) extraJemallocConfigureFlags="$extraJemallocConfigureFlags --with-lg-page=16" ;; \
	esac; \
	extraJemallocConfigureFlags="$extraJemallocConfigureFlags --with-lg-hugepage=21"; \
	grep -F 'cd jemalloc && ./configure ' /usr/src/redis/deps/Makefile; \
	sed -ri 's!cd jemalloc && ./configure !&'"$extraJemallocConfigureFlags"' !' /usr/src/redis/deps/Makefile; \
	grep -F "cd jemalloc && ./configure $extraJemallocConfigureFlags " /usr/src/redis/deps/Makefile; \
	\
	export BUILD_TLS=yes; \
	make -C /usr/src/redis -j "$(nproc)" all; \
	make -C /usr/src/redis install; \
	\
# TODO https://github.com/redis/redis/pull/3494 (deduplicate "redis-server" copies)
	serverMd5="$(md5sum /usr/local/bin/redis-server | cut -d' ' -f1)"; export serverMd5; \
	find /usr/local/bin/redis* -maxdepth 0 \
		-type f -not -name redis-server \
		-exec sh -eux -c ' \
			md5="$(md5sum "$1" | cut -d" " -f1)"; \
			test "$md5" = "$serverMd5"; \
		' -- '{}' ';' \
		-exec ln -svfT 'redis-server' '{}' ';' \
	; \
	\
	rm -r /usr/src/redis; \
	\
	apt-mark auto '.*' > /dev/null; \
	[ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \
	find /usr/local -type f -executable -exec ldd '{}' ';' \
		| awk '/=>/ { print $(NF-1) }' \
		| sort -u \
		| xargs -r dpkg-query --search \
		| cut -d: -f1 \
		| sort -u \
		| xargs -r apt-mark manual \
	; \
	apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
	\
	redis-cli --version; \
	redis-server --version

RUN mkdir /data && chown redis:redis /data  # 执行shell命令
VOLUME /data  # 设置一个挂载点(其实就是一个目录)
WORKDIR /data  # 运行容器后进入/data目录

COPY docker-entrypoint.sh /usr/local/bin/  # 把一个shell脚本拷贝到镜像内
ENTRYPOINT ["docker-entrypoint.sh"]  # 运行容器时执行

EXPOSE 6379  # 运行容器时暴露的端口
CMD ["redis-server"]  # 运行容器时执行的命令
5.3 自己写一个Dockerfile并构建出一个tomcat镜像

准备工作

moon@kumao:~/docker$ ls -l
total 156036
-rw-r--r-- 1 moon moon  11613418 Dec  7 17:08 apache-tomcat-9.0.70.tar.gz
-rw-r--r-- 1 moon moon 148162542 Dec  7 17:19 jdk-8u341-linux-x64.tar.gz
-rw-rw-r-- 1 moon moon         0 Dec  7 17:25 readme.txt
moon@kumao:~/docker$  

动手

# 基于ubuntu镜像
FROM ubuntu
# 作者
MAINTAINER wangguoqiang
#  拷贝readme文件到某个目录
COPY ./readme.txt /usr/local/readme.txt
# 添加jdk(构建时会自动解压)
ADD ./jdk-8u341-linux-x64.tar.gz /usr/local/
# 添加tomcat安装包 
ADD ./apache-tomcat-9.0.70.tar.gz /usr/local/

USER root
# 安装vim
RUN apt-get update && apt-get install -y vim
# 设置环境变量MYPATH
ENV MYPATH /usr/local
# 工作目录
WORKDIR $MYPATH

# 配置一些tomcat需要的环境
ENV JAVA_HOME /usr/local/jdk1.8.0_341
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV CATALINA_HOME /usr/local/apache-tomcat-9.0.70
ENV CATALINA_BASE /usr/local/apache-tomcat-9.0.70
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:CATALINA_HOME/bin

# 暴露端口
EXPOSE 8080

# 启动容器时执行的命令
CMD /usr/local/apache-tomcat-9.0.70/bin/startup.sh && tail -F /usr/local/apache-tomcat-9.0.70/bin/logs/catalina.out

构建出自己的tomcat镜像

docker build [OPTIONS] PATH | URL | -

docker build -f dockerfiles/Dockerfile.debug -t myapp_debug .  # 构建镜像(注意最后的.)
moon@kumao:~/docker$ ls
apache-tomcat-9.0.70.tar.gz  Dockerfile  jdk-8u341-linux-x64.tar.gz  readme.txt
moon@kumao:~/docker$ sudo docker build -t mytomcat:0.1 .
Sending build context to Docker daemon  159.8MB
Step 1/16 : FROM ubuntu
 ---> a8780b506fa4
# ......
Removing intermediate container b37e47033b89
 ---> 9bfe74230266
Successfully built 9bfe74230266
Successfully tagged mytomcat:0.1
# 成功构建出mytomcat:0.1镜像
moon@kumao:~/docker$ sudo docker images
REPOSITORY    TAG             IMAGE ID       CREATED              SIZE
mytomcat      0.1             9bfe74230266   About a minute ago   563MB
my_redis      1.0             efe0f631bd65   5 hours ago          117MB
redis         7.0.5           3e12e2ceb68f   28 hours ago         117MB
debian        bullseye-slim   9f94f3391d54   33 hours ago         80.5MB
ubuntu        latest          a8780b506fa4   4 weeks ago          77.8MB
hello-world   latest          feb5d9fea6a5   14 months ago        13.3kB
hello_world   1.0             feb5d9fea6a5   14 months ago        13.3kB
redis         5.0.7           7eed8df88d3b   2 years ago          98.2MB
# 通过自己构建的镜像启动tomcat容器,并测试可用
moon@kumao:~/docker$ sudo docker run -d -p 8888:8080 --name moontomcat mytomcat:0.1
aafba278a66f06782e19283a5e4913850159e1e30dcc1206214302d651291816
moon@kumao:~/docker$ curl 0.0.0.0:8888


<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>Apache Tomcat/9.0.70</title>
        <link href="favicon.ico" rel="icon" type="image/x-icon" />
        <link href="tomcat.css" rel="stylesheet" type="text/css" />
    </head>
    ......
</html>
6 Docker网络

参考这篇博客

6.1 Docker网络模式

Docker默认提供了3种网络模式,生成容器时不指定网络模式下默认使用bridge桥接模式

docker network ls  # 使用命令查看当前Docker所有的网络模式

root@kumao:/home/moon# docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
3484ceee917e   bridge    bridge    local
3b423e9b640f   host      host      local
8b3561ed22a6   none      null      local
  • host模式

如果启动容器时使用host模式,那么这个容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个Network Namespace。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。但是,容器的其他方面,如文件系统、进程列表等还是和宿主机隔离的。使用host模式的容器可以直接使用宿主机的IP地址与外界通信,容器内部的服务端口也可以使用宿主机的端口,host最大的优势就是网络性能比较好,但是docker host上已经使用的端口宿主机就不能再用了,网络的隔离性不好。

Namespace的简要说明:
Docker使用了Linux的Namespaces技术来进行资源隔离,如PID Namespace隔离进程,Mount Namespace隔离文件系统,Network Namespace隔离网络等。一个Network Namespace提供了一份独立的网络环境,包括网卡、路由、Iptable规则等都与其他的NetworkNamespace隔离。一个Docker容器一般会分配一个独立的Network Namespace

  • container模式

这个模式指定新创建的容器和已经存在的一个容器共享一个Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的IP,而是和一个指定的容器共享IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过lo网卡设备通信。

  • none模式

使用none模式,Docker容器拥有自己的Network Namespace,但是,并不为Docker容器进行任何网络配置。也就是说,这个Docker容器没有网卡、IP、路由等信息。需要我们自己为Docker容器添加网卡、配置IP等。这种网络模式下容器只有lo回环网络,没有其他网卡。none模式可以在容器创建时通过–network=none来指定。这种类型的网络没有办法联网,封闭的网络能很好的保证容器的安全性。

  • bridge模式

bridge模式是docker的默认网络模式,不写–net参数,就是bridge模式。使用docker run -p时,docker实际是在iptables做了DNAT规则,实现端口转发功能。可以使用iptables -t nat -vnL查看。

技术栈 —— Docker_第4张图片

当Docker进程启动时,会在主机上创建一个名为docker0的虚拟网桥,此主机上启动的Docker容器都会连接到这个虚拟网桥上。虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连在了一个二层网络中。
从docker0子网中分配一个IP给容器使用,并设置docker0的IP地址为容器的默认网关。在主机上创建一对虚拟网卡veth pair设备,Docker将veth pair设备的一端放在新创建的容器中,并命名为eth0(容器的网卡),另一端放在主机中,以vethxxx这样类似的名字命名,并将这个网络设备加入到docker0网桥中。可以通过brctl show命令查看。
技术栈 —— Docker_第5张图片
技术栈 —— Docker_第6张图片
从上图中我们可以看到,可以看到容器内部和Linux主机都会创建一个新的网卡,而这两个网卡都是成对的。使用的技术就是evth-pair。evth-pair 就是一对的虚拟设备接口,他们是成对出现的,一段连着协议,一段彼此相连。evth-pair充当一个桥梁,连接各种虚拟网络设备。
Docker容器完成bridge网络配置的过程如下:

  1. 在主机上创建一对虚拟网卡veth pair设备。veth设备总是成对出现的,它们组成了一个数据的通道,数据从一个设备进入,就会从另一个设备出来。因此,veth设备常用来连接两个网络设备。
  2. Docker将veth pair设备的一端放在新创建的容器中,并命名为eth0。另一端放在主机中,以vethb22db1b4这样类似的名字命名,并将这个网络设备加入到docker0网桥中。
  3. 从docker0子网中分配一个IP给容器使用,并设置docker0的IP地址为容器的默认网关。

宿主机和Docker容器之间是可以进行网络连接的,同样的,Docker容器和容器之间也可以直接进行网络连接。
技术栈 —— Docker_第7张图片
容器1和容器2,都使用公用的路由器docker0。所有的容器在不指定网络情况下,都是由docker0路由的,Docker会给我们容器默认分配一个随机的可用IP地址,这些IP地址之间是可以进行网络交互的。

6.2 容器互联

在微服务部署的场景下,注册中心是使用服务名来唯一识别微服务的,而我们上线部署的时候微服务对应的IP地址可能会改动,所以我们需要使用容器名来配置容器间的网络连接,使–link选项可以完成这个功能。
首先不设置–link的情况下,是无法通过容器名来进行连接的。ubuntu02容器是可以直接ping通ubuntu01,但是无法ping通ubuntu01的容器名。

root@kumao:/home/moon# docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
3484ceee917e   bridge    bridge    local
3b423e9b640f   host      host      local
8b3561ed22a6   none      null      local
root@kumao:/home/moon# docker network inspect 3484ceee917
# ...
"31cfcf19618076a82680553a888bd2bc8e9de80f63d0d86f229baea07112900a": {
                "Name": "ubuntu01",
                "EndpointID": "50d22b03e487c4778ffdea0d93bf01c8b187d666aed8ebccab7dd7f494a80aa0",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            },
"c1a5e763fe528b336fa02ffd9c722dbb8f689fd217435ebc042d2a3bf43b0f63": {
                "Name": "ubuntu02",
                "EndpointID": "cc2fa5386dd8fa9e140b6fc166445e9393404a1d706c5b9eb7a74a454249d47c",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": ""
            }
# ...

root@kumao:/home/moon# docker ps
CONTAINER ID   IMAGE     COMMAND       CREATED       STATUS       PORTS     NAMES
c1a5e763fe52   ubuntu    "/bin/bash"   2 hours ago   Up 2 hours             ubuntu02
31cfcf196180   ubuntu    "/bin/bash"   3 hours ago   Up 3 hours             ubuntu01
root@kumao:/home/moon# docker exec ubuntu02 ping 172.18.0.2
PING 172.18.0.2 (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.182 ms
64 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.184 ms
^C
root@kumao:/home/moon# docker exec ubuntu02 ping ubuntu01
ping: unknown host
root@kumao:/home/moon# 

添加参数–link,可以通过容器名进行连接。ubuntu03容器link到ubuntu01,所以ubuntu03可以直接通过容器名ping通ubuntu01,但是反过来ubuntu01去ping ubuntu03容器名是ping不通的。

root@kumao:/home/moon# docker run -it --name ubuntu03 --link ubuntu01 myubuntu
root@kumao:/home/moon# docker ps
CONTAINER ID   IMAGE      COMMAND       CREATED         STATUS         PORTS     NAMES
458748d7f67f   myubuntu   "/bin/bash"   9 seconds ago   Up 8 seconds             ubuntu03
c1a5e763fe52   ubuntu     "/bin/bash"   3 hours ago     Up 3 hours               ubuntu02
31cfcf196180   ubuntu     "/bin/bash"   3 hours ago     Up 3 hours               ubuntu01
root@kumao:/home/moon# docker exec ubuntu03 ping ubuntu01
PING ubuntu01 (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.219 ms
64 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.123 ms
64 bytes from 172.18.0.2: icmp_seq=2 ttl=64 time=0.148 ms
^C
root@kumao:/home/moon# docker exec ubuntu01 ping ubuntu03
ping: unknown host
root@kumao:/home/moon# 

–-link的原理就是在ubuntu03容器的hosts文件中添加了要去link的ubuntu01容器的容器名和ip地址映射。但是因为docker0不支持容器名访问,所以–-link设置容器互连的方式不推荐使用,更多地选择自定义网络。

技术栈 —— Docker_第8张图片

6.3 自定义网络
docker network --help  #  docker网络帮助命令
root@kumao:/home/moon# docker network --help

Usage:  docker network COMMAND

Manage networks

Commands:
  connect     Connect a container to a network
  create      Create a network
  disconnect  Disconnect a container from a network
  inspect     Display detailed information on one or more networks
  ls          List networks
  prune       Remove all unused networks
  rm          Remove one or more networks

Run 'docker network COMMAND --help' for more information on a command.
root@kumao:/home/moon# 
root@kumao:/home/moon# docker network create --help

Usage:  docker network create [OPTIONS] NETWORK

Create a network

Options:
      --attachable           Enable manual container attachment
      --aux-address map      Auxiliary IPv4 or IPv6 addresses used by Network driver (default map[])
      --config-from string   The network from which to copy the configuration
      --config-only          Create a configuration only network
  -d, --driver string        Driver to manage the Network (default "bridge")
      --gateway strings      IPv4 or IPv6 Gateway for the master subnet
      --ingress              Create swarm routing-mesh network
      --internal             Restrict external access to the network
      --ip-range strings     Allocate container ip from a sub-range
      --ipam-driver string   IP Address Management Driver (default "default")
      --ipam-opt map         Set IPAM driver specific options (default map[])
      --ipv6                 Enable IPv6 networking
      --label list           Set metadata on a network
  -o, --opt map              Set driver specific options (default map[])
      --scope string         Control the network's scope
      --subnet strings       Subnet in CIDR format that represents a network segment
root@kumao:/home/moon# 

自定义的docker网络mynet,网关192.158.0.1,子网192.158.0.0/16,后续创建容器时可用之。使用自定义网络创建容器后,使用相同网络下的容器,不管是通过容器IP还是容器名,都可以进行网络通信。

root@kumao:/home/moon# docker network create -d bridge --gateway 192.158.0.1 --subnet 192.158.0.0/16 mynet
1149ddd3e8874e55999a491a36983c2b807f9d9746dbd152102635b6fca5515a
root@kumao:/home/moon# docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
3484ceee917e   bridge    bridge    local
3b423e9b640f   host      host      local
1149ddd3e887   mynet     bridge    local
8b3561ed22a6   none      null      local
root@kumao:/home/moon# 
root@kumao:/home/moon# docker network inspect mynet
[
    {
        "Name": "mynet",
        "Id": "1149ddd3e8874e55999a491a36983c2b807f9d9746dbd152102635b6fca5515a",
        "Created": "2022-12-08T13:39:11.79885784+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "192.158.0.0/16",
                    "Gateway": "192.158.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "631f8ba964f47482cec295e4942e906bc4532a4a00e6dbf16ae399640e1403d8": {
                "Name": "ubuntu-net-02",
                "EndpointID": "4754e604d3010aa921424f04622e99cb7244eb69c045d2bf4deb8e8e8bf56839",
                "MacAddress": "02:42:c0:9e:00:03",
                "IPv4Address": "192.158.0.3/16",
                "IPv6Address": ""
            },
            "d0c6f9ebd6be415b5613835024025638ea0a7116b3fff12f5f38d57c4f77092e": {
                "Name": "ubuntu-net-01",
                "EndpointID": "98b6cd68bb14d85bac02e4cdb4df83f88ab760e0c6695274d996f85f919726f6",
                "MacAddress": "02:42:c0:9e:00:02",
                "IPv4Address": "192.158.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]
root@kumao:/home/moon#
root@kumao:/home/moon# docker run -it --name ubuntu-net-01 --net mynet myubuntu /bin/bash
root@kumao:/home/moon# docker run -it --name ubuntu-net-02 --net mynet myubuntu /bin/bash
root@kumao:/home/moon# docker ps
CONTAINER ID   IMAGE      COMMAND       CREATED              STATUS              PORTS     NAMES
631f8ba964f4   myubuntu   "/bin/bash"   57 seconds ago       Up 55 seconds                 ubuntu-net-02
d0c6f9ebd6be   myubuntu   "/bin/bash"   About a minute ago   Up About a minute             ubuntu-net-01
458748d7f67f   myubuntu   "/bin/bash"   32 minutes ago       Up 32 minutes                 ubuntu03
c1a5e763fe52   ubuntu     "/bin/bash"   3 hours ago          Up 3 hours                    ubuntu02
31cfcf196180   ubuntu     "/bin/bash"   3 hours ago          Up 3 hours                    ubuntu01
root@kumao:/home/moon# docker exec ubuntu-net-01 ping ubuntu-net-02
PING ubuntu-net-02 (192.158.0.3): 56 data bytes
64 bytes from 192.158.0.3: icmp_seq=0 ttl=64 time=0.115 ms
64 bytes from 192.158.0.3: icmp_seq=1 ttl=64 time=0.097 ms
64 bytes from 192.158.0.3: icmp_seq=2 ttl=64 time=0.100 ms
^C
root@kumao:/home/moon# docker exec ubuntu-net-02 ping ubuntu-net-01
PING ubuntu-net-01 (192.158.0.2): 56 data bytes
64 bytes from 192.158.0.2: icmp_seq=0 ttl=64 time=0.121 ms
64 bytes from 192.158.0.2: icmp_seq=1 ttl=64 time=0.133 ms
64 bytes from 192.158.0.2: icmp_seq=2 ttl=64 time=0.152 ms
^C
root@kumao:/home/moon# docker exec ubuntu-net-01 ping 192.158.0.3
PING 192.158.0.3 (192.158.0.3): 56 data bytes
64 bytes from 192.158.0.3: icmp_seq=0 ttl=64 time=0.174 ms
64 bytes from 192.158.0.3: icmp_seq=1 ttl=64 time=0.157 ms
^C
6.4 网络联通

在没有使用connect命令的情况下,不同网络间的容器是无法进行网络连接的

技术栈 —— Docker_第9张图片

root@kumao:/home/moon# docker exec ubuntu01 ping ubuntu-net-02
ping: unknown host
root@kumao:/home/moon# 

不同Docker网络之间的容器想要连接的话,需要把该容器注册到另一个容器所在的网络上,使用docker connect命令

root@kumao:/home/moon# docker network connect --help

Usage:  docker network connect [OPTIONS] NETWORK CONTAINER

Connect a container to a network

Options:
      --alias strings           Add network-scoped alias for the container
      --driver-opt strings      driver options for the network
      --ip string               IPv4 address (e.g., 172.30.100.104)
      --ip6 string              IPv6 address (e.g., 2001:db8::33)
      --link list               Add link to another container
      --link-local-ip strings   Add a link-local address for the container
root@kumao:/home/moon# docker network connect mynet ubuntu01
root@kumao:/home/moon# 
root@kumao:/home/moon# docker network inspect mynet
[
    {
        "Name": "mynet",
        "Id": "1149ddd3e8874e55999a491a36983c2b807f9d9746dbd152102635b6fca5515a",
        "Created": "2022-12-08T13:39:11.79885784+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "192.158.0.0/16",
                    "Gateway": "192.158.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "31cfcf19618076a82680553a888bd2bc8e9de80f63d0d86f229baea07112900a": {
                "Name": "ubuntu01",  # 其实就是把ubuntu01加入到mynet网络,这时ubuntu01有两个IP
                "EndpointID": "abad6fbcb038cf631974e2e3cd20c23effb26ee7072f226fb514aa697ce30f60",
                "MacAddress": "02:42:c0:9e:00:04",
                "IPv4Address": "192.158.0.4/16",
                "IPv6Address": ""
            },
            "631f8ba964f47482cec295e4942e906bc4532a4a00e6dbf16ae399640e1403d8": {
                "Name": "ubuntu-net-02",
                "EndpointID": "4754e604d3010aa921424f04622e99cb7244eb69c045d2bf4deb8e8e8bf56839",
                "MacAddress": "02:42:c0:9e:00:03",
                "IPv4Address": "192.158.0.3/16",
                "IPv6Address": ""
            },
            "d0c6f9ebd6be415b5613835024025638ea0a7116b3fff12f5f38d57c4f77092e": {
                "Name": "ubuntu-net-01",
                "EndpointID": "98b6cd68bb14d85bac02e4cdb4df83f88ab760e0c6695274d996f85f919726f6",
                "MacAddress": "02:42:c0:9e:00:02",
                "IPv4Address": "192.158.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]
root@kumao:/home/moon# 
root@kumao:/home/moon# docker exec ubuntu01 ping ubuntu-net-01
PING ubuntu-net-01 (192.158.0.2): 56 data bytes
64 bytes from 192.158.0.2: icmp_seq=0 ttl=64 time=0.230 ms
64 bytes from 192.158.0.2: icmp_seq=1 ttl=64 time=0.158 ms
^C
root@kumao:/home/moon# docker exec ubuntu01 ping ubuntu-net-02
PING ubuntu-net-02 (192.158.0.3): 56 data bytes
64 bytes from 192.158.0.3: icmp_seq=0 ttl=64 time=0.197 ms
64 bytes from 192.158.0.3: icmp_seq=1 ttl=64 time=0.201 ms
^C
root@kumao:/home/moon# docker exec ubuntu-net-01 ping ubuntu01
PING ubuntu01 (192.158.0.4): 56 data bytes
64 bytes from 192.158.0.4: icmp_seq=0 ttl=64 time=0.214 ms
64 bytes from 192.158.0.4: icmp_seq=1 ttl=64 time=0.219 ms
64 bytes from 192.158.0.4: icmp_seq=2 ttl=64 time=0.217 ms
^C

你可能感兴趣的:(docker,容器,运维)