2.1.1 安装Docker并运行Hello World容器
基本内容基于原书,代码部分稍有修改,以适应最新版本
CentOS一键安装docker
curl -fsSL https://get.docker.com | bash -s docker --mirror aliyun
开启docker并设置自启动
sudo systemctl start docker
systemctl enable docker
VirtualBox 动态磁盘扩容 vboxmanage modifyhd "/home/matrix/VirtualBox VMs/ubuntu/ubuntu.vdi" --resize 10240
参考连接 https://www.cnblogs.com/xueweihan/p/5923937.html#1
运行Hello World容器
busybox是一个单一可执行文件,包含多种标准UNIX命令行工具,如:echo、ls、gzip 等。除了包含 echo 命令的 busybox 命令,也可以使用如Fedora、Ubuntu等功能完备的镜像。
如何才能运行 busybox 镜像呢?无须下载或者安装任何东西。使用 docker run
命令然后指定需要运行的镜像的名字,以及需要执行的命令(可选),如下面这段代码。
代码清单2.1 使用Docker运行一个Hello world容器
[root@localhost test]# docker run busybox echo 'Hello world'
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
b71f96345d44: Pull complete
Digest: sha256:930490f97e5b921535c153e0e7110d251134cc4b72bbb8133c6a5065cc68580d
Status: Downloaded newer image for busybox:latest
Hello world
背后的原理
首先,Docker会检查busybox:latest 镜像是否已经存在于本机。如果没有,Docker会从http://docker.io的Docker镜像中心拉取镜像。镜像下载到本机之后,Docker基于这个镜像创建一个容器并在容器中运行命令。echo 命令打印文字到标准输出流,然后进程终止,容器停止运行。
运行其他镜像
运行其他的容器镜像和运行busybox镜像是一样的,甚至可能更简单,因为你可以不需要指定执行命令。就像例子中的 echo "Hello world",被执行的命令通常都会被包含在镜像中,但也可以根据需要进行覆盖。在浏览器中搜索http://hub.docker.com或其他公开的镜像中心的可用镜像之后,可以像这样在Docker中运行镜像:
$ docker run
容器镜像的版本管理
当然,所有的软件包都会更新,所以通常每个包都不止一个版本。Docker支持同一镜像的多个版本。每一个版本必须有唯一的tag名。当引用镜像没有显式地指定tag时,Docker会默认指定tag为latest。如果想要运行别的版本的镜像,需要像这样指定镜像的版本:
$ docker run :
2.1.2 创建一个简单的 Node.js 应用
现在有了一个可以工作的Docker环境来创建应用。接下来会构建一个简单的Node.js Web应用,并把它打包到容器镜像中。这个应用会接收HTTP请求并响应应用运行的主机名。这样,应用运行在容器中,看到的是自己的主机名而不是宿主机名,即使它也像其他进程一样运行在宿主机上。这在后面会非常有用,当应用部署在Kubernetes上并进行伸缩时(水平伸缩,复制应用到多个节点),你会发现HTTP请求切换到了应用的不同实例上。
应用包含一个名为app.js的文件,详见下面的代码清单。
代码清单2.2 一个简单的Node.js应用:app.js
const http = require('http');
const os = require('os');
const hostname = '0.0.0.0';
const port = 888;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end("You've hit " + os.hostname() + "\n");
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
代码清晰地说明了实现的功能。这里在8080端口启动了一个HTTP服务器。服务器会以状态码 200 OK
和文字 "You've hit
来响应每个请求。请求handler会把客户端的IP打印到标准输出,以便日后查看。注意 返回的主机名是服务器真实的主机名,不是客户端发出的HTTP请求中头的 Host 字段。
现在可以直接下载安装Node.js来测试代码了,但是这不是必需的,因为可以直接用Docker把应用打包成镜像,这样在需要运行的主机上就无须下载和安装其他的东西(当然不包括安装Docker来运行镜像)。
2.1.3 为镜像创建Dockerfile
为了把应用打包成镜像,首先需要创建一个叫Dockerfile的文件,它包含了一系列构建镜像时会执行的指令。Dockerfile文件需要和app.js文件在同一目录,并包含下面代码清单中的命令。
代码清单2.3 构建应用容器镜像的Dockerfile
FROM node:12
ADD app.js /app.js
ENTRYPOINT ["node","app.js"]
From 行定义了镜像的起始内容(构建所基于的基础镜像)。这个例子中使用的是 node 镜像的tag 7 版本。第二行中把app.js文件从本地文件夹添加到镜像的根目录,保持app.js这个文件名。最后一行定义了当镜像被运行时需要被执行的命令,这个例子中,命令是 node app.js。
选择基础镜像
你或许在想,为什么要选择这个镜像作为基础镜像。因为这个应用是Node.js应用,镜像需要包含可执行的 node 二进制文件来运行应用。你也可以使用任何包含这个二进制文件的镜像,或者甚至可以使用Linux发行版的基础镜像,如 fedora或ubuntu,然后在镜像构建的时候安装Node.js。但是由于 node镜像是专门用来运行Node.js应用的,并且包含了运行应用所需的一切,所以把它当作基础镜像。
2.1.4 构建容器镜像
现在有了Dockerfile和app.js文件,这是用来构建镜像的所有文件。运行下面的Docker命令来构建镜像:
$ docker build -t kubia .
删除镜像必须删除容器
//查询容器
#docker ps -a
//删除容器
#docker rm 67jhgf***
//查询镜像
#docker images
//删除镜像
#docker rmi 99fkjh***
用户告诉Docker需要基于当前目录(注意命令结尾的点)构建一个叫kubia的镜像,Docker会在目录中寻找Dockerfile,然后基于其中的指令构建镜像。
镜像是如何构建的
构建过程不是由Docker客户端进行的,而是将整个目录的文件上传到Docker守护进程并在那里进行的。Docker客户端和守护进程不要求在同一台机器上。如果你在一台非Linux操作系统中使用Docker,客户端就运行在你的宿主操作系统上,但是守护进程运行在一个虚拟机内。由于构建目录中的文件都被上传到了守护进程中,如果包含了大量的大文件而且守护进程不在本地运行,上传过程会花费更多的时间。
提示 不要在构建目录中包含任何不需要的文件,这样会减慢构建的速度——尤其当Docker守护进程运行在一个远端机器的时候。
在构建过程中,Docker首次会从公开的镜像仓库(Docker Hub)拉取基础镜像(node:12),除非已经拉取过镜像并存储在本机上了。
镜像分层
镜像不是一个大的二进制块,而是由多层组成的,在运行busybox例子时你可能已经注意到(每一层有一行Pull complete),不同镜像可能会共享分层,这会让存储和传输变得更加高效。比如,如果创建了多个基于相同基础镜像(比如例子中的 node:7)的镜像,所有组成基础镜像的分层只会被存储一次。拉取镜像的时候,Docker会独立下载每一层。一些分层可能已经存储在机器上了,所以Docker只会下载未被存储的分层。
你或许会认为每个Dockerfile只创建一个新层,但是并不是这样的。构建镜像时,Dockerfile中每一条单独的指令都会创建一个新层。镜像构建的过程中,拉取基础镜像所有分层之后,Docker在它们上面创建一个新层并且添加app.js。然后会创建另一层来指定镜像被运行时所执行的命令。最后一层会被标记为kubia:latest。图2.3 展示了这个过程,同时也展示另外一个叫 other:latest 的镜像如何与我们构建的镜像共享同一层Node.js镜像。
构建完成时,新的镜像会存储在本地。下面的代码展示了如何通过Docker列出本地存储的镜像:
代码清单2.4 列出本地存储的镜像
s docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
kubia latest d30ecc7419e7 1 minute ago 637.1 MB
比较使用Dockerfile和手动构建镜像
Dockerfile是使用Docker构建容器镜像的常用方式,但也可以通过运行已有镜像容器来手动构建镜像,在容器中运行命令,退出容器,然后把最终状态作为新镜像。用Dockerfile构建镜像是与此相同的,但是是自动化且可重复的,随时可以通过修改Dockerfile重新构建镜像而无须手动重新输入命令。
2.1.5 运行容器镜像
以下的命令可以用来运行镜像:
$ docker run --name kube-container -p 888:888 -d kubia
关闭后运行的话
$ docker restart kube-container
这条命令告知Docker基于 kubia 镜像创建一个叫 kubia-container 的新容器。这个容器与命令行分离(-d 标志),这意味着在后台运行。本机上的888端口会被映射到容器内的888端口(-p 888:888 选项),所以可以通过http://localhost:888
访问这个应用。
如果没有在本机上运行Docker守护进程(比如使用的是Mac或Windows系统,守护进程会运行在VM中),需要使用VM的主机名或IP来代替localhost运行守护进程。可以通过 DOCKER_HOST 这个环境变量查看主机名。
访问应用
现在试着通过 http://localhost:888
访问你的应用(确保使用Docker主机名或IP替换localhost):
$ curl localhost:888
You've hit 44d76963e8e1
这是应用的响应。现在应用运行在容器中,与其他东西隔离。可以看到,应用把 44d76963e8e1 作为主机名返回,这并不是宿主机的主机名。这个十六进制数是Docker容器的ID。
列出所有运行中的容器
下面的代码清单列出了所有的运行中的容器,可以查看列表(为了更好的可读性,列表被分成了两行显示)。
代码清单2.5 列出运行中的容器
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9d3e6e24ab3c kubia "node app.js" 4 minutes ago Up 4 minutes 0.0.0.0:8080->8080/tcp kubia-container
有一个容器在运行。Docker会打印出每一个容器的ID和名称、容器运行所使用的镜像,以及容器中执行的命令。
获取更多的容器信息
docker ps 只会展示容器的大部分基础信息。可以使用 docker inspect查看更多的信息:
$ docker inspect kubia-container
Docker会打印出包含容器底层信息的长JSON。
2.1.6 探索运行容器的内部
我们来看看容器内部的环境。由于一个容器里可以运行多个进程,所以总是可以运行新的进程去看看里面发生了什么。如果镜像里有可用的shell二进制可执行文件,也可以运行一个shell。
在已有的容器内部运行shell
镜像基于的Node.js镜像包含了bash shell,所以可以像这样在容器内运行shell:
$ docker exec -it kubia-container bash
这会在已有的kubia-container容器内部运行bash。bash 进程会和主容器进程拥有相同的命名空间。这样可以从内部探索容器,查看Node.js和应用是如何在容器里运行的。-it 选项是下面两个选项的简写:
-
-i
,确保标准输入流保持开放。需要在shell中输入命令。 -
-t
,分配一个伪终端(TTY)。
如果希望像平常一样使用shell,需要同时使用这两个选项(如果缺少第一个选项就无法输入任何命令。如果缺少第二个选项,那么命令提示符不会显示,并且一些命令会提示 TERM 变量没有设置)。
从内部探索容器
下面的代码展示了如何使用shell查看容器内运行的进程。
代码清单2.6 从容器内列出进程
# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.7 565712 30100 ? Ssl 08:22 0:00 node app.js
root 14 0.1 0.0 18188 3344 pts/0 Ss 08:25 0:00 bash
root 22 0.0 0.0 36640 2764 pts/0 R+ 08:25 0:00 ps -aux
只看到了三个进程,宿主机上没有看到其他进程。
容器内的进程运行在主机操作系统上
如果现在打开另一个终端,然后列出主机操作系统上的进程,连同其他的主机进程依然会发现容器内的进程,如代码清单2.7所示。
注意 如果使用的是Mac或者Windows系统,需要登录到Docker守护进程运行的VM查看这些进程。
代码清单2.7 运行在主机操作系统上的容器进程
$ ps aux | grep app.js
USER PID CPU MEM vsz RSS TTY STAT START TIME COMMAND
root 382 0.0 0.1 676380 16504 ? S1 12:31 0:00 node app.js
这证明了运行在容器中的进程是运行在主机操作系统上的。如果你足够敏锐,会发现进程的ID在容器中与主机上不同。容器使用独立的PID Linux命名空间并且有着独立的系列号,完全独立于进程树。
容器的文件系统也是独立的
正如拥有独立的进程树一样,每个容器也拥有独立的文件系统。在容器内列出根目录的内容,只会展示容器内的文件,包括镜像内的所有文件,再加上容器运行时创建的任何文件(类似日志文件),如下面的代码清单所示。
代码清单2.8 容器拥有完整的文件系统
root@44d76963e8el:/# 1s
app.js boot etc lib mediabin dev home 1ib64 mnt
opt root sbin sys usrpros run srv tmp var
其中包含app.js文件和其他系统目录,这些目录是正在使用的 node:7 基础镜像的一部分。可以使用 exit 命令来退出容器返回宿主机(类似于登出ssh session)。
提示 进入容器对于调试容器内运行的应用来说是非常有用的。出错时,需要做的第一件事是查看应用运行的系统的真实状态。需要记住的是,应用不仅拥有独立的文件系统,还有进程、用户、主机名和网络接口。
2.1.7 停止和删除容器
可以通过告知Docker停止 kubia-container 容器来停止应用:
$ docker stop kubia-container
因为没有其他的进程在容器内运行,这会停止容器内运行的主进程。容器本身仍然存在并且可以通过 docker ps-a 来查看。-a 选项打印出所有的容器,包括运行中的和已经停止的。想要真正地删除一个容器,需要运行 docker rm :
$ docker rm kubia-container
这会删除容器,所有的内容会被删除并且无法再次启动。
2.1.8 向镜像仓库推送镜像
现在构建的镜像只可以在本机使用。为了在任何机器上都可以使用,可以把镜像推送到一个外部的镜像仓库。为了简单起见,不需要搭建一个私有的镜像仓库,而是可以推送镜像到公开可用的Docker Hub(http://hub.docker.com)镜像中心。另外还有其他广泛使用的镜像中心,如阿里云镜像服务
登陆阿里云个人版的镜像服务
sudo docker login --username={你的邮箱} registry.cn-hangzhou.aliyuncs.com
使用附加标签标注镜像
一旦知道了自己的ID,就可以重命名镜像,现在镜像由 kubia 改为 registry.cn-hangzhou.aliyuncs.com/bernard/kubia(其中bernard是我自己定义的命名空间):
$ docker tag kubia registry.cn-hangzhou.aliyuncs.com/bernard/kubia
这不会重命名标签,而是给同一个镜像创建一个额外的标签。可以通过docker images 命令列出本机存储的镜像来加以确认,如下面的代码清单所示。
代码清单2.9 一个容器镜像可以有多个标签
# docker images | head
REPOSITORY TAG IMAGE ID CREATED SIZE
kubia v2 8af4d2d1750a 2 minutes ago 82.7MB
正如所看到的,kubia 和 改为 registry.cn-hangzhou.aliyuncs.com/bernard/kubia 指向同一个镜像ID,所以实际上是同一个镜像的两个标签。
向阿里云镜像服务推送镜像
$ docker push registry.cn-hangzhou.aliyuncs.com/bernard/kubia
在不同机器上运行镜像
在推送完成之后,镜像便可以给任何人使用。可以在任何机器上运行下面的命令来运行镜像:
$ docker run -p 888:888 registry.cn-hangzhou.aliyuncs.com/bernard/kubia
这非常简单。最棒的是应用每次都运行在完全一致的环境中。如果在你的机器上正常运行,也会在所有的Linux机器上正常运行。无须担心主机是否安装了Node.js。事实上,就算安装了,应用也并不会使用,因为它使用的是镜像内部安装的。