3. 镜像和容器的基本操作

这节课给大家讲解Docker镜像和容器的一些基本操作方法。

获取镜像

之前我们提到过 Docker 官方提供了一个公共的镜像仓库:Docker Hub,我们就可以从这上面获取镜像,获取镜像的命令:docker pull,格式为:

  1. $ docker pull [选项] [Docker Registry 地址[:端口]/]仓库名[:标签]
  • Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号],默认地址是 Docker Hub。
  • 仓库名:这里的仓库名是两段式名称,即 <用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。比如:
    1. $ docker pull ubuntu:16.04
    2. 16.04: Pulling from library/ubuntu
    3. bf5d46315322: Pull complete
    4. 9f13e0ac480c: Pull complete
    5. e8988b5b3097: Pull complete
    6. 40af181810e7: Pull complete
    7. e6f7c7e5c03e: Pull complete
    8. Digest: sha256:147913621d9cdea08853f6ba9116c2e27a3ceffecf3b492983ae97c3d643fbbe
    9. Status: Downloaded newer image for ubuntu:16.04
    上面的命令中没有给出 Docker 镜像仓库地址,因此将会从 Docker Hub 获取镜像。而镜像名称是 ubuntu:16.04,因此将会获取官方镜像 library/ubuntu 仓库中标签为 16.04 的镜像。 从下载过程中可以看到我们之前提及的分层存储的概念,镜像是由多层存储所构成。下载也是一层层的去下载,并非单一文件。下载过程中给出了每一层的 ID 的前 12 位。并且下载结束后,给出该镜像完整的sha256的摘要,以确保下载一致性。

运行

有了镜像后,我们就能够以这个镜像为基础启动并运行一个容器。以上面的 ubuntu:16.04 为例,如果我们打算启动里面的 bash 并且进行交互式操作的话,可以执行下面的命令。

  1. $ docker run -it --rm \
  2. ubuntu:16.04 \
  3. /bin/bash
  4. root@e7009c6ce357:/# cat /etc/os-release
  5. NAME="Ubuntu"
  6. VERSION="16.04.4 LTS, Trusty Tahr"
  7. ID=ubuntu
  8. ID_LIKE=debian
  9. PRETTY_NAME="Ubuntu 16.04.4 LTS"
  10. VERSION_ID="16.04"
  11. HOME_URL="http://www.ubuntu.com/"
  12. SUPPORT_URL="http://help.ubuntu.com/"
  13. BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"

docker run就是运行容器的命令,具体格式我们会在后面的课程中进行详细讲解,我们这里简要的说明一下上面用到的参数。

  • -it:这是两个参数,一个是 -i:交互式操作,一个是 -t 终端。我们这里打算进入 bash 执行一些命令并查看返回结果,因此我们需要交互式终端。
  • —rm:这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 docker rm。我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用--rm可以避免浪费空间。
  • ubuntu:16.04:这是指用 ubuntu:16.04 镜像为基础来启动容器。
  • bash:放在镜像名后的是命令,这里我们希望有个交互式 Shell,因此用的是 bash。

进入容器后,我们可以在 Shell 下操作,执行任何所需的命令。这里,我们执行了cat /etc/os-release,这是 Linux 常用的查看当前系统版本的命令,从返回的结果可以看到容器内是 Ubuntu 16.04.4 LTS 系统。最后我们通过 exit 退出了这个容器。

列出镜像

  1. $ docker image ls

列表包含了仓库名、标签、镜像 ID、创建时间以及所占用的空间。镜像 ID 则是镜像的唯一标识,一个镜像可以对应多个标签。

镜像大小

如果仔细观察,会注意到,这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同。比如,ubuntu:16.04 镜像大小,在这里是 127 MB,但是在Docker Hub显示的却是 43 MB。这是因为 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。而docker image ls显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。

另外一个需要注意的问题是,docker image ls列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。你可以通过以下命令来便捷的查看镜像、容器、数据卷所占用的空间。

  1. $ docker system df

新建并启动

所需要的命令主要为docker run。 例如,下面的命令输出一个 “Hello World”,之后终止容器。

  1. $ docker run ubuntu:16.04 /bin/echo 'Hello world'
  2. Hello world

这跟在本地直接执行/bin/echo 'hello world'几乎感觉不出任何区别。下面的命令则启动一个 bash 终端,允许用户进行交互。

  1. $ docker run -t -i ubuntu:16.04 /bin/bash
  2. root@af8bae53bdd3:/#

其中,-t选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上,-i则让容器的标准输入保持打开。 在交互模式下,用户可以通过所创建的终端来输入命令,例如:

  1. root@af8bae53bdd3:/# pwd
  2. /
  3. root@af8bae53bdd3:/# ls
  4. bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

当利用docker run来创建容器时,Docker 在后台运行的标准操作包括:

  • 检查本地是否存在指定的镜像,不存在就从公有仓库下载
  • 利用镜像创建并启动一个容器
  • 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
  • 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
  • 从地址池配置一个 ip 地址给容器
  • 执行用户指定的应用程序
  • 执行完毕后容器被终止

启动已终止容器

可以利用docker container start命令,直接将一个已经终止的容器启动运行。

容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必需的。除此之外,并没有其它的资源。可以在伪终端中利用 ps 或 top 来查看进程信息。

  1. root@ba267838cc1b:/# ps
  2. PID TTY TIME CMD
  3. 1 ? 00:00:00 bash
  4. 11 ? 00:00:00 ps

可见,容器中仅运行了指定的 bash 应用。这种特点使得 Docker 对资源的利用率极高,是货真价实的轻量级虚拟化。

后台运行

更多的时候,需要让 Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加-d参数来实现。下面举两个例子来说明一下。

如果不使用-d参数运行容器。

  1. $ docker run ubuntu:16.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
  2. hello world
  3. hello world
  4. hello world
  5. hello world

容器会把输出的结果 (STDOUT) 打印到宿主机上面。如果使用了-d参数运行容器。

  1. $ docker run -d ubuntu:16.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
  2. 77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a

此时容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用 docker logs 查看)。

注: 容器是否会长久运行,是和 docker run 指定的命令有关,和 -d 参数无关。

使用-d参数启动后会返回一个唯一的 id,也可以通过docker container ls命令来查看容器信息。

  1. $ docker container ls
  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  3. 77b2dc01fe0f ubuntu:16.04 /bin/sh -c 'while tr 2 minutes ago Up 1 minute agitated_wright
  4. 要获取容器的输出信息,可以通过 docker container logs 命令。
  5. $ docker container logs [container ID or NAMES]
  6. hello world
  7. hello world
  8. hello world
  9. . . .

终止容器

可以使用docker container stop来终止一个运行中的容器。此外,当 Docker 容器中指定的应用终结时,容器也自动终止。

例如对于上一章节中只启动了一个终端的容器,用户通过 exit 命令或 Ctrl+d 来退出终端时,所创建的容器立刻终止。终止状态的容器可以用docker container ls -a 命令看到。例如

  1. $ docker container ls -a
  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  3. ba267838cc1b ubuntu:16.04 "/bin/bash" 30 minutes ago Exited (0) About a minute ago trusting_newton

处于终止状态的容器,可以通过docker container start命令来重新启动。

此外,docker container restart命令会将一个运行态的容器终止,然后再重新启动它。

进入容器

在使用-d参数时,容器启动后会进入后台。某些时候需要进入容器进行操作:exec 命令 -i -t 参数

只用-i参数时,由于没有分配伪终端,界面没有我们熟悉的Linux命令提示符,但命令执行结果仍然可以返回。 当-i -t参数一起使用时,则可以看到我们熟悉的 Linux命令提示符。

  1. $ docker run -dit ubuntu:16.04
  2. 69d137adef7a8a689cbcb059e94da5489d3cddd240ff675c640c8d96e84fe1f6
  3. $ docker container ls
  4. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  5. 69d137adef7a ubuntu:16.04 "/bin/bash" 18 seconds ago Up 17 seconds zealous_swirles
  6. $ docker exec -i 69d1 bash
  7. ls
  8. bin
  9. boot
  10. dev
  11. ...
  12. $ docker exec -it 69d1 bash
  13. root@69d137adef7a:/#

如果从这个 stdin 中 exit,不会导致容器的停止。这就是为什么推荐大家使用docker exec的原因。

更多参数说明请使用docker exec --help查看。

删除容器

可以使用docker container rm来删除一个处于终止状态的容器。例如:

  1. $ docker container rm trusting_newton
  2. trusting_newton

也可用使用docker rm容器名来删除,如果要删除一个运行中的容器,可以添加-f参数。Docker 会发送 SIGKILL信号给容器。

docker container ls -a (或者docker ps -a)命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器。

  1. $ docker container prune

或者

  1. $ docker ps -aq

删除本地镜像

如果要删除本地的镜像,可以使用`docker image rm·命令,其格式为:

  1. $ docker image rm [选项] <镜像1> [<镜像2> ...]

或者

  1. $ docker rmi 镜像名

或者用 ID、镜像名、摘要删除镜像 其中,<镜像> 可以是 镜像短 ID、镜像长 ID、镜像名 或者 镜像摘要。 比如我们有这么一些镜像:

  1. $ docker image ls
  2. REPOSITORY TAG IMAGE ID CREATED SIZE
  3. centos latest 0584b3d2cf6d 3 weeks ago 196.5 MB
  4. redis alpine 501ad78535f0 3 weeks ago 21.03 MB
  5. docker latest cf693ec9b5c7 3 weeks ago 105.1 MB
  6. nginx latest e43d811ce2f4 5 weeks ago 181.5 MB

我们可以用镜像的完整 ID,也称为 长 ID,来删除镜像。使用脚本的时候可能会用长 ID,但是人工输入就太累了,所以更多的时候是用 短 ID 来删除镜像。docker image ls默认列出的就已经是短 ID 了,一般取前3个字符以上,只要足够区分于别的镜像就可以了。

比如这里,如果我们要删除redis:alpine镜像,可以执行:

  1. $ docker image rm 501
  2. Untagged: redis:alpine
  3. Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
  4. Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
  5. Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b
  6. Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23
  7. Deleted: sha256:127227698ad74a5846ff5153475e03439d96d4b1c7f2a449c7a826ef74a2d2fa
  8. Deleted: sha256:1333ecc582459bac54e1437335c0816bc17634e131ea0cc48daa27d32c75eab3
  9. Deleted: sha256:4fc455b921edf9c4aea207c51ab39b10b06540c8b4825ba57b3feed1668fa7c7

我们也可以用镜像名,也就是 <仓库名>:<标签>,来删除镜像。

  1. $ docker image rm centos
  2. Untagged: centos:latest
  3. Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
  4. Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a
  5. Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38

docker commit定制镜像

镜像是容器的基础,每次执行docker run的时候都会指定哪个镜像作为容器运行的基础。在之前的例子中,我们所使用的都是来自于 Docker Hub 的镜像。直接使用这些镜像是可以满足一定的需求,而当这些镜像无法直接满足需求时,我们就需要定制这些镜像。接下来的几节就将讲解如何定制镜像。

回顾一下之前我们学到的知识,镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。

现在让我们以定制一个 Web 服务器为例子,来讲解镜像是如何构建的。

  1. $ docker run --name webserver -d -p 80:80 nginx

这条命令会用 nginx 镜像启动一个容器,命名为 webserver,并且映射了 80 端口,这样我们可以用浏览器去访问这个 nginx 服务器。

如果是在 Linux 本机运行的 Docker,或者如果使用的是 Docker for Mac、Docker for Windows,那么可以直接访问:http://localhost;如果使用的是 Docker Toolbox,或者是在虚拟机、云服务器上安装的 Docker,则需要将 localhost 换为虚拟机地址或者实际云服务器地址。

直接用浏览器访问的话,我们会看到默认的 Nginx 欢迎页面。

现在,假设我们非常不喜欢这个欢迎页面,我们希望改成欢迎 Docker 的文字,我们可以使用 docker exec命令进入容器,修改其内容。

  1. $ docker exec -it webserver bash
  2. root@3729b97e8226:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  3. root@3729b97e8226:/# exit

我们以交互式终端方式进入 webserver 容器,并执行了 bash 命令,也就是获得一个可操作的 Shell。 然后,我们用<h1>Hello, Docker!</h1>覆盖了 /usr/share/nginx/html/index.html的内容。 现在我们再刷新浏览器的话,会发现内容被改变了。

我们修改了容器的文件,也就是改动了容器的存储层。我们可以通过docker diff命令看到具体的改动。

  1. $ docker diff webserver
  2. C /root
  3. A /root/.bash_history
  4. C /run
  5. C /usr
  6. C /usr/share
  7. C /usr/share/nginx
  8. C /usr/share/nginx/html
  9. C /usr/share/nginx/html/index.html
  10. C /var
  11. C /var/cache
  12. C /var/cache/nginx
  13. A /var/cache/nginx/client_temp
  14. A /var/cache/nginx/fastcgi_temp
  15. A /var/cache/nginx/proxy_temp
  16. A /var/cache/nginx/scgi_temp
  17. A /var/cache/nginx/uwsgi_temp

现在我们定制好了变化,我们希望能将其保存下来形成镜像。

要知道,当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个docker commit命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。

我们可以用下面的命令将容器保存为镜像:

  1. $ docker commit \
  2. --author "海马学院" \
  3. --message "修改了默认首页" \
  4. webserver \
  5. nginx:v2
  6. sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa1795214

其中--author是指定修改的作者,而--message则是记录本次修改的内容。这点和 git 版本控制相似,不过这里这些信息可以省略留空。

我们可以在docker image ls中看到这个新定制的镜像:

  1. $ docker image ls nginx
  2. REPOSITORY TAG IMAGE ID CREATED SIZE
  3. nginx v2 07e334659748 9 seconds ago 181.5 MB
  4. nginx 1.11 05a60462f8ba 12 days ago 181.5 MB
  5. nginx latest e43d811ce2f4 4 weeks ago 181.5 MB

我们还可以用docker history具体查看镜像内的历史记录,如果比较 nginx:latest 的历史记录,我们会发现新增了我们刚刚提交的这一层。

  1. $ docker history nginx:v2
  2. IMAGE CREATED CREATED BY SIZE COMMENT
  3. 07e334659748 54 seconds ago nginx -g daemon off; 95 B 修改了默认网页
  4. e43d811ce2f4 4 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon 0 B
  5. <missing> 4 weeks ago /bin/sh -c #(nop) EXPOSE 443/tcp 80/tcp 0 B
  6. <missing> 4 weeks ago /bin/sh -c ln -sf /dev/stdout /var/log/nginx/ 22 B
  7. <missing> 4 weeks ago /bin/sh -c apt-key adv --keyserver hkp://pgp. 58.46 MB
  8. <missing> 4 weeks ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.11.5-1 0 B
  9. <missing> 4 weeks ago /bin/sh -c #(nop) MAINTAINER NGINX Docker Ma 0 B
  10. <missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B
  11. <missing> 4 weeks ago /bin/sh -c #(nop) ADD file:23aa4f893e3288698c 123 MB

新的镜像定制好后,我们可以来运行这个镜像。

  1. $ docker run --name webserv2 -d -p 81:80 nginx:v2

这里我们命名为新的服务为 webserv2,并且映射到 81 端口。如果是 Docker for Mac/Windows 或 Linux 桌面的话,我们就可以直接访问 http://localhost:81 看到结果,其内容应该和之前修改后的 webserver 一样。

至此,我们第一次完成了定制镜像,使用的是docker commit命令,手动操作给旧的镜像添加了新的一层,形成新的镜像,对镜像多层存储应该有了更直观的感觉。

注意: docker commit 命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用 docker commit 定制镜像,定制镜像应该使用Dockerfile来完成。如果你想要定制镜像请查看下一小节。


点击查看本文视频

扫描下面的二维码(或微信搜索k8s技术圈)关注我们的微信公众帐号,在微信公众帐号中回复 加群 即可加入到我们的 kubernetes 讨论群里面共同学习。

k8s技术圈二维码