07 实战演练:玩转Docker
你好,我是Chrono。
学到今天的这次课,我们的"入门篇"就算是告一段落了,有这些容器知识作为基础,很快我们就要正式开始学习Kubernetes。不过在那之前,来对前面的课程做一个回顾和实践,把基础再夯实一下。
要提醒你的是,Docker相关的内容很多很广,在入门篇中,我只从中挑选出了一些最基本最有用的介绍给你。而且在我看来,我们不需要完全了解Docker的所有功能,我也不建议你对Docker的内部架构细节和具体的命令行参数做过多的了解,太浪费精力,只要会用够用,需要的时候能够查找官方手册就行。
毕竟我们这门课程的目标是Kubernetes,而Docker只不过是众多容器运行时(Container Runtime)中最出名的一款而已。当然,如果你当前的工作是与Docker深度绑定,那就另当别论了。
好下面我先把容器技术做一个简要的总结,然后演示两个实战项目:使用Docker部署Registry和WordPress。
容器技术要点回顾
容器技术是后端应用领域的一项重大创新,它彻底变革了应用的开发、交付与部署方式,是"云原生"的根本(01讲)。
容器基于Linux底层的namespace、cgroup、chroot等功能,虽然它们很早就出现了,但直到Docker"横空出世",把它们整合在一起,容器才真正走近了大众的视野,逐渐为广大开发者所熟知(02讲)。
容器技术中有三个核心概念:容器(Container) 、镜像(Image) ,以及镜像仓库(Registry) (03讲)。

从本质上来说,容器属于虚拟化技术的一种,和虚拟机(Virtual Machine)很类似,都能够分拆系统资源,隔离应用进程,但容器更加轻量级,运行效率更高,比虚拟机更适合云计算的需求。
镜像是容器的静态形式,它把应用程序连同依赖的操作系统、配置文件、环境变量等等都打包到了一起,因而能够在任何系统上运行,免除了很多部署运维和平台迁移的麻烦。
镜像内部由多个层(Layer)组成,每一层都是一组文件,多个层会使用Union FS技术合并成一个文件系统供容器使用。这种细粒度结构的好处是相同的层可以共享、复用,节约磁盘存储和网络传输的成本,也让构建镜像的工作变得更加容易(04讲)。
为了方便管理镜像,就出现了镜像仓库,它集中存放各种容器化的应用,用户可以任意上传下载,是分发镜像的最佳方式(05讲)。
目前最知名的公开镜像仓库是Docker Hub,其他的还有quay.io、gcr.io,我们可以在这些网站上找到许多高质量镜像,集成到我们自己的应用系统中。
容器技术有很多具体的实现,Docker是最初也是最流行的容器技术,它的主要形态是运行在Linux上的"Docker Engine"。我们日常使用的 docker 命令其实只是一个前端工具,它必须与后台服务"Docker daemon"通信才能实现各种功能。
操作容器的常用命令有 docker ps、docker run、docker exec、docker stop 等;操作镜像的常用命令有 docker images、docker rmi、docker build、docker tag 等;操作镜像仓库的常用命令有 docker pull、docker push 等。
好简单地回顾了容器技术,下面我们就来综合运用在"入门篇"所学到的各个知识点,开始实战演练,玩转Docker。
搭建私有镜像仓库
在第5节课讲Docker Hub的时候曾经说过,在离线环境里,我们可以自己搭建私有仓库。但因为镜像仓库是网络服务的形式,当时还没有学到容器网络相关的知识,所以只有到了现在,我们具备了比较完整的Docker知识体系,才能够搭建私有仓库。
私有镜像仓库有很多现成的解决方案,今天我只选择最简单的Docker Registry,而功能更完善的CNCF Harbor留到后续学习Kubernetes时再介绍。
你可以在Docker Hub网站上搜索"registry",找到它的官方页面(https://registry.hub.docker.com/_/registry/):

Docker Registry的网页上有很详细的说明,包括下载命令、用法等,我们可以完全照着它来操作。
首先,你需要使用 docker pull 命令拉取镜像:
docker pull registry
然后,我们需要做一个端口映射,对外暴露端口,这样Docker Registry才能提供服务。它的容器内端口是5000,简单起见,我们在外面也使用同样的5000端口,所以运行命令就是 docker run -d -p 5000:5000 registry :
docker run -d -p 5000:5000 registry
启动Docker Registry之后,你可以使用 docker ps 查看它的运行状态,可以看到它确实把本机的5000端口映射到了容器内的5000端口。

接下来,我们就要使用 docker tag 命令给镜像打标签再上传了。因为上传的目标不是默认的Docker Hub,而是本地的私有仓库,所以镜像的名字前面还必须再加上仓库的地址(域名或者IP地址都行),形式上和HTTP的URL非常像。
比如在这里,我就把"nginx:alpine"改成了"127.0.0.1:5000/nginx:alpine":
docker tag nginx:alpine 127.0.0.1:5000/nginx:alpine
现在,这个镜像有了一个附加仓库地址的完整名字,就可以用 docker push 推上去了:
docker push 127.0.0.1:5000/nginx:alpine

为了验证是否已经成功推送,我们可以把刚才打标签的镜像删掉,再重新下载:
docker rmi 127.0.0.1:5000/nginx:alpine docker pull 127.0.0.1:5000/nginx:alpine

这里 docker pull 确实完成了镜像下载任务,不过因为原来的层原本就已经存在,所以不会有实际的下载动作,只会创建一个新的镜像标签。
Docker Registry虽然没有图形界面,但提供了RESTful API,也可以发送HTTP请求来查看仓库里的镜像,具体的端点信息可以参考官方文档(Deprecated and retired Docker products and features | Docker Docs),下面的这两条curl命令就分别获取了镜像列表和Nginx镜像的标签列表:
curl 127.1:5000/v2/_catalog curl 127.1:5000/v2/nginx/tags/list

可以看到,因为应用被封装到了镜像里,所以我们只用简单的一两条命令就完成了私有仓库的搭建工作,完全不需要复杂的软件安装、环境设置、调试测试等繁琐的操作,这在容器技术出现之前简直是不可想象的。
搭建WordPress网站
Docker Registry应用比较简单,只用单个容器就运行了一个完整的服务,下面我们再来搭建一个有点复杂的WordPress网站。
网站需要用到三个容器:WordPress、MariaDB、Nginx,它们都是非常流行的开源项目,在Docker Hub网站上有官方镜像,网页上的说明也很详细,所以具体的搜索过程我就略过了,直接使用 docker pull 拉取它们的镜像:
docker pull wordpress:5 docker pull mariadb:10 docker pull nginx:alpine
我画了一个简单的网络架构图,你可以直观感受一下它们之间的关系:

这个系统可以说是比较典型的网站了。MariaDB作为后面的关系型数据库,端口号是3306;WordPress是中间的应用服务器,使用MariaDB来存储数据,它的端口是80;Nginx是前面的反向代理,它对外暴露80端口,然后把请求转发给WordPress。
我们先来运行MariaDB。根据说明文档,需要配置"MARIADB_DATABASE"等几个环境变量,用 --env 参数来指定启动时的数据库、用户名和密码,这里我指定数据库是"db",用户名是"wp",密码是"123",管理员密码(root password)也是"123"。
下面就是启动MariaDB的 docker run 命令:
docker run -d --rm \ --env MARIADB_DATABASE=db \ --env MARIADB_USER=wp \ --env MARIADB_PASSWORD=123 \ --env MARIADB_ROOT_PASSWORD=123 \ mariadb:10
启动之后,我们还可以使用 docker exec 命令,执行数据库的客户端工具"mysql",验证数据库是否正常运行:
docker exec -it 9ac mysql -u wp -p
输入刚才设定的用户名"wp"和密码"123"之后,我们就连接上了MariaDB,可以使用 show databases; 和 show tables; 等命令来查看数据库里的内容。当然,现在肯定是空的。

因为Docker的bridge网络模式的默认网段是"172.17.0.0/16",宿主机固定是"172.17.0.1",而且IP地址是顺序分配的,所以如果之前没有其他容器在运行的话,MariaDB容器的IP地址应该就是"172.17.0.2",这可以通过 docker inspect 命令来验证:
docker inspect 9ac |grep IPAddress

现在数据库服务已经正常,该运行应用服务器WordPress了,它也要用 --env 参数来指定一些环境变量才能连接到MariaDB,注意"WORDPRESS_DB_HOST"必须是MariaDB的IP地址,否则会无法连接数据库:
docker run -d --rm \ --env WORDPRESS_DB_HOST=172.17.0.2 \ --env WORDPRESS_DB_USER=wp \ --env WORDPRESS_DB_PASSWORD=123 \ --env WORDPRESS_DB_NAME=db \ wordpress:5
WordPress容器在启动的时候并没有使用 -p 参数映射端口号,所以外界是不能直接访问的,我们需要在前面配一个Nginx反向代理,把请求转发给WordPress的80端口。
配置Nginx反向代理必须要知道WordPress的IP地址,同样可以用 docker inspect 命令查看,如果没有什么意外的话它应该是"172.17.0.3",所以我们就能够写出如下的配置文件(Nginx的用法可参考其他资料,这里就不展开讲了):
server { listen 80; default_type text/html; location / { proxy_http_version 1.1; proxy_set_header Host $host; proxy_pass http://172.17.0.3; } }
有了这个配置文件,最关键的一步就来了,我们需要用 -p 参数把本机的端口映射到Nginx容器内部的80端口,再用 -v 参数把配置文件挂载到Nginx的"conf.d"目录下。这样,Nginx就会使用刚才编写好的配置文件,在80端口上监听HTTP请求,再转发到WordPress应用:
docker run -d --rm \ -p 80:80 \ -v `pwd`/wp.conf:/etc/nginx/conf.d/default.conf \ nginx:alpine
三个容器都启动之后,我们再用 docker ps 来看看它们的状态:

可以看到,WordPress和MariaDB虽然使用了80和3306端口,但被容器隔离,外界不可见,只有Nginx有端口映射,能够从外界的80端口收发数据,网络状态和我们的架构图是一致的。
现在整个系统就已经在容器环境里运行好了,我们来打开浏览器,输入本机的"127.0.0.1"或者是虚拟机的IP地址(我这里是"http://192.168.10.208"),就可以看到WordPress的界面:

在创建基本的用户、初始化网站之后,我们可以再登录MariaDB,看看是否已经有了一些数据:

可以看到,WordPress已经在数据库里新建了很多的表,这就证明我们的容器化的WordPress网站搭建成功。
小结
好了,今天我们简单地回顾了一下容器技术,这里有一份思维导图,是对前面所有容器知识要点的总结,你可以对照着用来复习。

我们还使用Docker实际搭建了两个服务:Registry镜像仓库和WordPress网站。
通过这两个项目的实战演练,你应该能够感受到容器化对后端开发带来的巨大改变,它简化了应用的打包、分发和部署,简单的几条命令就可以完成之前需要编写大量脚本才能完成的任务,对于开发、运维来绝对是一个"福音"。
不过,在感受容器便利的同时,你有没有注意到它还是存在一些遗憾呢?比如说:
- 我们还是要手动运行一些命令来启动应用,然后再人工确认运行状态。
- 运行多个容器组成的应用比较麻烦,需要人工干预(如检查IP地址)才能维护网络通信。
- 现有的网络模式功能只适合单机,多台服务器上运行应用、负载均衡该怎么做?
- 如果要增加应用数量该怎么办?这时容器技术完全帮不上忙。
其实,如果我们仔细整理这些运行容器的 docker run 命令,写成脚本,再加上一些Shell、Python编程来实现自动化,也许就能够得到一个勉强可用的解决方案。
这个方案已经超越了容器技术本身,是在更高的层次上规划容器的运行次序、网络连接、数据持久化等应用要素,也就是现在我们常说的"容器编排"(Container Orchestration)的雏形,也正是后面要学习的Kubernetes的主要出发点。
课下作业
最后是课下作业时间,给你留两个思考题:
- 学完了"入门篇",和刚开始相比,你对容器技术有了哪些更深入的思考和理解?
- 你觉得容器编排应该解决哪些方面的问题?
欢迎积极留言讨论,如果有收获,也欢迎你转发给身边的朋友一起学习。
下节课是视频课,我会用视频直观演示我们前面学过的操作,我们下节课见。

精选留言(15)
-
lesserror 👍(17) 💬(3) 之前对docker的了解很杂乱,知识点很细碎、分散,没有一个整体、清晰的认知。 看过中文互联网上面别人的一些教程,要么照本宣科,要么浅尝辄止。 老师的课程虽然没有做到知识点的面面俱到,当然也不可能做到。但是,算是整体上帮我又重新梳理了一遍docker的整体架构,让我对其认识更加清晰了一些。
2022-07-06
-
pyhhou 👍(10) 💬(1) 思考题: 1. 相较于之前只知道容器是用来环境隔离,看完入门篇后,对容器技术有了一个比较宏观和基本的了解,列出来如下: 1)知道了什么是镜像,以及镜像和容器的关系 2)知道了 DockerHub 这样的镜像仓库 3)明白了容器和虚拟机的不同 4)懂得如何通过 Dockerfile 来构建自己的镜像 5)理解了 Docker 的整体内部框架 docker client -> docker daemon -> registry 6) 知道了,也实际操作了一些常用的镜像以及容器相关的指令 。。。 感觉学习到的这些东西可以覆盖工作中大多数的场景了,但是这些知识只能说是运用于小规模的东西。想要把容器技术玩的得心应手,还需了解一些容器应用的最佳实践,和一些工程化的理念和工具 2. 感觉容器编排主要应用于大规模集成应用。可以类比分布式系统,入门篇中讲的知识用在单机应用上是没有问题的,但是规模一旦变大到系统层面,就会出现一些问题,比如如何保证数据一致性?如何保证负载均衡?如何尽可能减少网络故障所带来的影响?如何能保证数据(容器)的持久化等等。。。这些问题需要运用容器编排来解决 另外想请教老师 2 个问题 文章一开始提到容器运行时(Container Runtime)这个概念,该如何理解?这是和容器绑定的一门技术吗? 还有就是,我看你在 curl 指令中直接将本地 IP 127.0.0.1 简写成 127.1,是说 curl 中允许这样的简写,还是说这本身就是一个惯例? 谢谢老师 🙏
2022-07-10
-
朱雯 👍(6) 💬(1) q1: 容器编排技术是有价值的,我之前以为价值不大,只是改变启动和使用方式,增加一些命令。 q2: 容器编排解决的问题是:一些非自动化,而是需要强人工干预的东西,比如网络交互需要知道对方ip地址的情况,虽然可以写自动化脚本,但这个并不通用,所以是一套通用的自动化方案。另外多台机器,自动创建负载均衡,创建路由的配置问题。这些是编排的范围。
08 视频:入门篇实操总结
你好,我是Chrono。
今天的课程和前面的不太一样,变成了视频的形式。之前也讲过很多次学习Kubernetes要以动手实操为主,加上专栏里单纯的文字配图的形式还是不太直观,所以每到一个学习阶段,我们就会来一个视频总结,把之前学习的内容以视频的形式展现出来,这样也许会让学习的效果更好。
这次视频课程的主要内容和第7讲差不多,是对"入门篇"的回顾与总结,但侧重点是对Docker的实际操作,不会再重复讲那些理论知识。每个视频后都会附上操作要点,供你快速定位和做笔记。
好了,我们正式开始吧。
一. 熟悉Docker的使用
视频操作要点:
首先来操作一下Docker Engine。
(有了课前准备的基础)在这台机器上,Docker已经安装好了,我给你用 docker version 和 docker info 看一下它的信息。
docker version 显示的是Docker Engine 20.10.12,系统是Linux,硬件架构是arm64,也就是Apple M1。
docker info 显示的是当前系统相关的信息,例如CPU、内存、容器数量、镜像数量、容器运行时、存储文件系统等等。这里存储用的文件系统是overlay2,Linux内核是5.13,操作系统是Ubuntu 22.04 Jammy Jellyfish,硬件是aarch64,两个CPU,内存4G。
现在我们用 docker ps 看一下容器列表,应该是空的。
然后用 docker pull 拉取busybox镜像,再用 docker images 看镜像列表。
使用 docker run 启动busybox镜像,执行最简单的hello world:
docker run busybox echo hello world
然后再用 docker ps -a 查看已经结束的容器列表,应该可以看到刚刚运行完毕的Busybox容器,可以用 docker rm 再加上容器ID删除它。
二. 镜像和容器
视频操作要点:
我们再来拉取另一个镜像,操作系统Alpine:
docker pull alpine
然后用 docker run,加上it参数,运行它里面的shell:
docker run -it alpine sh
这样就暂时离开当前的Ubuntu操作系统,进入了容器内部的Alpine系统,可以在里面执行任意的命令,比如 cat /etc/os-release 。
这个容器环境与外面是完全隔离的,进程、文件系统都独立,不过也有没有隔离的部分,比如时间和内核。
使用exit退出容器,然后在宿主机环境执行date、uname -a,你就可以看到它与容器里是一致的。
让我们再运行一个容器:
docker run -d --rm nginx:alpine
在宿主机里用 ps -ef|grep nginx 可以看到有3个Nginx进程,它们其实就是容器里的Nginx进程,用docker stop停止后再用ps,就能发现它们已经消失了。
这就证明,容器其实就是操作系统里的进程,只是被容器运行环境加上了namespace、cgroup、chroot的限制,所以容器和普通进程在资源的使用方面是没有什么区别的,也因为没有虚拟机的成本,启动更迅速,资源利用率也就更高。
三. 构建自己的镜像
视频操作要点:
现在让我们来尝试编写Dockerfile,构建一个自己的镜像。
这个Dockerfile先用arg指令定义了IMAGE_BASE、IMAGE_TAG两个变量,然后使用from指令指定了构建的基础镜像,把这两个变量结合起来就是 nginx:1.21-alpine 。
后面的两个env指令定义了PATH和DEBUG两个环境变量。arg指令定义的变量只能在构建镜像的时候使用,而env定义的变量则会在容器运行的时候以环境变量的形式出现,让进程运行时使用。
接下来是copy指令,它会把构建上下文里的./default.conf拷贝到镜像的/etc/nginx/conf.d/,注意copy指令不能使用绝对路径,必须是构建上下文的相对路径,而且Docker会把构建上下文里的所有文件打包传递给docker daemon,所有尽量只包含必要的文件。
run指令就是构建镜像时要执行的shell命令,可以是安装软件、创建目录、编译程序等等,这里只是简单地用echo命令生成了一个文本文件。
最后两条指令是 expose 和 workdir,expose 是声明容器对外服务的端口号,而 workdir 是容器的工作目录。
了解了Dockerfile的内容之后,我们就要用 docker build 来构建镜像了,使用 -t 打上标签,再加上构建上下文路径,当前目录就用一个点号 .:
docker build -t ngx-app:1.0 .
构建完成,生成镜像文件,我们可以用 docker run 从镜像启动容器,验证镜像里的文件是否正确生成:
docker run -it --rm ngx-app:1.0 sh
然后我们还可以用 docker save/load 命令把它导出成压缩包,方便保存和传输:
docker save ngx-app:1.0 -o ngx.tar docker load -i ngx.tar
四. 与外部系统互通的操作
视频操作要点:
接下来我们看看容器与外部系统互通的一些操作。
首先是 docker cp 命令。让我们先启动一个Redis容器:
docker run -d --rm redis
然后用 echo 命令生成一个简单的文本文件:
echo 'aaa' > a.txt
用 docker ps 命令看看容器的ID,就可以使用 docker cp 命令把这个文件拷贝进容器里了:
docker cp a.txt 062:/tmp
使用 docker exec 可以进入容器内部,查看文件是否已经正确拷贝:
docker exec -it 062 sh ls /tmp
退出容器,我们再把这个文件改个名字,拷贝出来:
docker cp 062:/tmp/a.txt ./b.txt
现在我们再看看用 -v 参数直接挂载本地目录,把文件直接映射到容器内部:
docker run -d --rm -v /tmp:/tmp redis
用 docker exec 进入容器,查看一下容器内的"/tmp"目录,应该就可以看到文件与宿主机是完全一致的。
docker exec -it b5a sh # b5a是容器ID
-p 参数是映射本机端口到容器端口,我们启动一个Nginx容器,把本机的80端口映射到容器的80端口:
docker run -d -p 80:80 --rm nginx:alpine
docker ps 可以看到端口的映射情况,我们也可以使用curl直接访问容器里的Nginx服务:
curl 127.1:80 -I
再使用exec就可以看到容器里的网卡情况:
docker exec xxx ip addr
可以发现容器里的网卡设置与宿主机完全不同,eth0是一个虚拟网卡,IP地址是B类私有地址"172.17.0.2"。
五. 搭建WordPress
视频操作要点:
最后演示一下使用Docker搭建WordPress的过程。
因为在Docker命令里写环境变量很麻烦,命令很长,所以我把搭建的过程写成了一个脚本。
一共有三条命令,首先启动MariaDB,设置数据库名、用户名、密码等环境变量,然后启动WordPress,使用刚才的MariaDB的用户名、密码,db_host必须是MariaDB的IP地址,然后再启动Nginx,它需要在配置文件里指定WordPress的地址,然后用-v参数挂载进容器里。
执行这个脚本之后,我们用 docker ps 看一下容器的状态。
确认容器都运行正常,我们就可以在浏览器里输入IP地址,访问WordPress网站了。
课下作业
今天是动手操作课,作业就是一定记得让自己实际上手操作一遍哦。
欢迎在留言区分享自己的实操感受,如果有什么疑问也欢迎留言分享参与讨论。我们下节课初级篇见。
精选留言(15)
- Geek_4d5ba0 👍(2) 💬(1) 老师讲的太赞了,一个上午看完入门篇,其中的细节讲的特别好,容易理解,像--rm,rm,rmi简直了,把删除镜像,删除容器,删除未运行状态的容器讲的一清二楚,还有容器仓库也是,mariadb这个数据库名字都讲到了。期待老师的后续课程