Docker 容器开发:虚拟化

Docker 的核心价值在于虚拟化或者说环境隔离【通过虚拟化技术实现虚拟环境】,解决环境配置和部署的依赖问题实现解耦

我对虚拟化的理解源自《Operating Systems: Three Easy Pieces》,推荐阅读

容器技术相关历史推荐知乎上的这篇文章:容器技术的历史

Docker 基本概念

Docker 提供了在称为容器的松散隔离环境中打包和运行应用程序的能力

  • 镜像(Image):镜像是一个只读的模板,用于创建 Docker 容器
  • 容器(Container):容器是一个可运行的实例,是镜像的运行时实例

两者的关系类似于面向对象编程中的类和对象

Docker 生态系统

  • Docker 注册表(Docker Registry):Docker 注册表是一个存储和分发 Docker 镜像的场所
  • Docker 客户端(Docker Client):用于与 Docker 服务器交互的 CLI 工具
  • Docker 服务器(Docker Server):Docker 服务器是一个守护进程,用于管理 Docker 对象,如镜像、容器、网络和卷
  • Docker Hub:这是所有自定义镜像的存储库,类似于 GitHub

一种技术的发展离不开生态系统的支持,Docker 的生态系统也是 Docker 成功的重要原因之一,像 Git 与 GitHub、Node.js 与 npm、Python 与 PyPI 等技术都有类似的生态系统

关于 Docker 安装,这里不再赘述,可以参考官方文档:Get Docker

不过我比较好奇的是 Docker 是基于 Linux 的 Kernal 实现的,那么在 Windows 和 Mac 上如何运行呢?反正因为这个在 Windows 和 Mac 安装会相对麻烦一点。我只是练习使用是在虚拟机中使用 Debian 安装的

只不过需要说明一下,一般在安装 Docker 后,关于 Docker 相关内容需要使用 sudo 进行提权,如果想方便使用应该将你想使用的用户添加到 docker 组中,命令:sudo usermod -aG docker $USER

Docker 使用容器

通过镜像创建容器

一般在创建容器的过程:

  • Docker 守护进程首先尝试在本地仓库查找 Image
  • 如果本地没有对应 Image 会继续远程拉取
  • 在 Docker 守护进程成功获取 Image,后会通过 Image 创建 Container

示例:

# hello-world 镜像是用于测试的,通过下面命令可以创建对应容器并运行此容器
docker run hello-world


#### 下面是会显示的内容 #####

# 这是本地没有 hello-world 镜像
Unable to find image 'hello-world:latest' locally

# 下面是真的进行远程拉取,并创建容器
latest: Pulling from library/hello-world
2db29710123e: Pull complete 
Digest: sha256:ffb13da98453e0f04d33a6eee5bb8e46ee50d08ebe17735fc0779d0349e889e9
Status: Downloaded newer image for hello-world:latest

# 下面是容器运行的内容,除了首行外我使用 ... 做了省略 ????
Hello from Docker!
...
  • 其中的拉取操作可以单独进行,使用 docker pull <images-name>[:<version>]

  • 其中 run 的语法大致是 docker run <images-name>[:<version>]

  • 使用 docker ps 可以查看所有 正在运行的容器,通过添加 -a 选项可以查看所有可用dd

    简单说明,列表头的含义

    • CONTAINER ID:显示每个容器的唯一 ID
    • IMAGE:创建容器的图像
    • COMMAND:启动时在容器中执行的命令
    • CREATED:容器被创建的时间
    • STATUS:容器的当前状态
    • PORTS:如果任何容器端口映射情况
    • NAMES:这是容器的名称,如果没有设置会随机生成一个并且是唯一的

说明一下

  • <> 表示里面的内容是变换的
  • [] 表示里面的内容是可选的

接下来,需要简单说明一下镜像中的层,就是 hello-world 中 2db29710123e: Pull complete 做一下解释:

  • 由于每个镜像都构建在 Linux 内核之上,因此它具有一些可以被其他镜像重用的共同依赖项
  • Docker 将这些依赖项捆绑在一个堆栈中,这些堆栈称为层
  • 只有指令 RUN、COPY、ADD 创建层,其他指令创建临时中间图像并且不会增加构建的大小
# 测试拉取 Nginx 这种相对 hello-world 大的镜像
docker pull nginx

#### 结果 ####
Using default tag: latest
latest: Pulling from library/nginx

# 被分五层
f1f26f570256: Pull complete 
7f7f30930c6b: Pull complete 
2836b727df80: Pull complete 
e1eeb0f1c06b: Pull complete 
86b2457cc2b0: Pull complete 
9862f2ee2e8c: Pull complete 
Digest: sha256:2ab30d6ac53580a6db8b657abf0f68d75360ff5cc1670a85acb5bd85ba1b19c0
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
  • 分层不取决于镜像大小,它的决定性因素是设计者,在于共同依赖项的设计上,有时一个小镜像也会有大量分层

交互模式的容器,我们以 python:3.6 为例,先创建对应容器 docker run python:3.6

  • 在使用 ps 查看时会发现没有容器,那是因为容器并没有运行,所以需要使用 -a 选项

    CONTAINER ID   IMAGE         COMMAND     CREATED             STATUS                         PORTS     NAMES
    8cc71dccbb99   python:3.6    "python3"   10 minutes ago      Exited (0) 10 minutes ago                cool_mendel
    fba35ad4d200   hello-world   "/hello"    About an hour ago   Exited (0) About an hour ago             elastic_wing
    
  • 们就一直在说 Docker 使用 Linux 内核作为容器,我们创建使用 bash 交互模式的容器

    在运行容器时,使用 -it 选项【实际是两个选项联用】,语法是:docker run -it <image-name> bash

    • -t 完整选项是 --tty:作用是分配一个伪 tty
    • -i 完整选项是 --interactive:作用是保持STDIN打开【即使未连接】,其实从 interactive 的中文意思就可以理解
# 创建使用 bash 交互式 tty 的 python:3.6 容器
docker run -it python:3.6 bash

#### 结果:目前处于虚拟 Linux 操作系统中 ####
root@84779de65a0b:/#

#### 交互操作过程 ####

# 使用 id 确认自己权限身份
root@84779de65a0b:/# id
uid=0(root) gid=0(root) groups=0(root)

# 退出 exit,提示如果退出容器它将停止,想要验证使用 docker ps 看有没有容器运行
root@84779de65a0b:/# exit
exit
learn@debian10:~$
  • 现在我们能够创建可以访问 bash 的容器

如何再次使用停止的 Docker 容器

通过 docker ps -a 我们已经有三个容器,现在学习如何让停止的容器运行

docker ps -a

CONTAINER ID   IMAGE         COMMAND     CREATED          STATUS                      PORTS     NAMES
84779de65a0b   python:3.6    "bash"      8 minutes ago    Exited (0) 3 minutes ago              romantic_meninsky
8cc71dccbb99   python:3.6    "python3"   37 minutes ago   Exited (0) 37 minutes ago             cool_mendel
fba35ad4d200   hello-world   "/hello"    2 hours ago      Exited (0) 11 minutes ago             elastic_wing
    • 语法是 docker start <container-id>,我们选择 romantic_meninsky【之前通过 docker run -it python:3.6 bash 创建的容器】,类比推理如果要停止运行容器 docker stop <container-id>,重启使用 docker restart <container-id>

      learn@debian10:~$ docker start 84779de65a0b
      84779de65a0b
      learn@debian10:~$ docker ps
      CONTAINER ID   IMAGE        COMMAND   CREATED          STATUS         PORTS     NAMES
      84779de65a0b   python:3.6   "bash"    16 minutes ago   Up 6 seconds             romantic_meninsky
      
  • romantic_meninsky 容器再次运行了,通过 docker exec -it <container-id|container-name> bash 可以重新登录容器的 bash,解释说明 | 是或的意思在其中表示使用容器 ID 或容器名称,比如:docker exec -it 84779de65a0b bash 等价 docker exec -it romantic_meninsky bash

docker exec -it 84779de65a0b bash

#### 结果:再次进入容器 romantic_meninsky ###
root@84779de65a0b:/#

#### 容器内操作 ####

# 我们将在容器中安装一些东西,以便我们可以在其中编写一些 python 脚本
# 修改 apt 源【国内镜像快一点】,命令 sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
root@84779de65a0b:/# sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list

# 更新 apt 缓存,命令 apt update
root@84779de65a0b:/# apt update

# 安装一个编辑器,随意 vim, nano, ... 选你熟悉的
root@84779de65a0b:/# apt install vim

# 编辑器创建 hello-world.py
root@84779de65a0b:/# vim hello-world.py

#### vim 编辑器内容,进入编辑器按 i 键会进入编辑器模式 ####
print("Hello World")
#### vim End,按 Esc 键切换模式,再按 : 键输入 wq 即可保存退出 ####

# 运行 hello-world.py,当然这只是测试用,也可以写其他 Python 程序并运行
root@84779de65a0b:/# python hello-world.py
Hello World

# 退出
root@84779de65a0b:/# exit
exit
  • 我们可以通过这样的方法对容器进行开发
  • 在实际开发环境中还会与一些编辑器进行联合使用,比如 Visual Studio Code

通过容器创建镜像

在经过这些操作后,容器内部已经发生变动,现在我们将这个容器打包为镜像,语法 docker commit -m "<commit-message>" <container-id|name> <new-image-name>:<version>,这个有点像 Git 的提交操作,其中 -m 是添加注释信息的

docker commit -m "python36 Hello World Test" 84779de65a0b my-python36-hello-world:1.0

#### 结果:会输出一段 SHA256 作为生成镜像的哈希校验 ####
sha256:2f12c4d8f189633c4b0b58e6496f429f125a125cdd47e016991af721569a763e

# 通过 docker images 可以查看本地镜像仓库
docker images

REPOSITORY                TAG       IMAGE ID       CREATED         SIZE
my-python36-hello-world   1.0       2f12c4d8f189   2 minutes ago   958MB
nginx                     latest    080ed0ed8312   7 days ago      142MB
python                    3.6       54260638d07c   15 months ago   902MB
hello-world               latest    feb5d9fea6a5   18 months ago   13.3kB

通过下面操作可以将本地镜像推送到远程仓库 Docker Hub 中

  • 你需要一个自己的 Docker Hub 帐户,并进行登录 docker login -u <username>,建议登录时使用 Access Tokens 方式进行【在 https://hub.docker.com/ 上设置】,登出 docker logout
    • 设置 tag 标签,语法 docker tag <image-name>:<version> <username>/<image>:<version> 对于 tag 标签作用有点类似虚拟机拍摄快照,相当于镜像的快照,实际应该以版本为单位使用 tag 标签记录
  • 使用推送,语法 docker push <username>/<image>:<version>
# 设置 tag 标签 docker tag my-python36-hello-world:1.0 shadow7749/my-python36-hello-world:1.0
learn@debian10:~$ docker tag my-python36-hello-world:1.0 shadow7749/my-python36-hello-world:1.0
# 查看本地镜像仓库变化
learn@debian10:~$ docker images
REPOSITORY                           TAG       IMAGE ID       CREATED          SIZE
my-python36-hello-world              1.0       2f12c4d8f189   29 minutes ago   958MB
shadow7749/my-python36-hello-world   1.0       2f12c4d8f189   29 minutes ago   958MB
nginx                                latest    080ed0ed8312   7 days ago       142MB
python                               3.6       54260638d07c   15 months ago    902MB
hello-world                          latest    feb5d9fea6a5   18 months ago    13.3kB
# 推送,此操作必须确认你已经登录
learn@debian10:~$ docker push shadow7749/my-python36-hello-world:1.0
The push refers to repository [docker.io/shadow7749/my-python36-hello-world]
ff540de539ad: Pushed 
aa4c808c19f6: Mounted from library/python 
8ba9f690e8ba: Mounted from library/python 
3e607d59ef9f: Mounted from library/python 
1e18e7e1fcc2: Mounted from library/python 
c3a0d593ed24: Mounted from library/python 
26a504e63be4: Mounted from library/python 
8bf42db0de72: Mounted from library/python 
31892cc314cb: Mounted from library/python 
11936051f93b: Mounted from library/python 
1.0: digest: sha256:790bdc67737ed40e747fa9f1b7fb2831ac09031811036d2c9bda3a4b4eb56b94 size: 2430

/// 这便是一个简单的 Docker 容器开发流程:

拉取镜像 --> 创建容器 --> 开发容器 --> 生成镜像 --> tag 标签 --> 推送镜像
                ^                                    |
                |------------- 继续开发 --------------|

补充说明:删除容器和镜像

  • 删除容器,需要先停止容器,然后使用 docker rm <container-id>
  • 清理掉所有处于终止状态的容器的快捷方式 docker container prune
  • 删除镜像使用 docker rmi <image-id>,记忆上镜像的删除无非是多个 i 表示 image

后续内容:Docker 容器数据:持久化