Docker实践(26) – 设置从Dockerfile指定点缓存失效

使用–no-cache构建镜像大多情况下足够解决由于缓存引起的问题。不过有时候你想要一个更细粒度的解决方案。例如你构建的镜像需要时间比较长,你仍然想一些步骤使用缓存,然后从指定的点开始不使用缓存重新运行命令构建镜像。

问题

你想在构建镜像时设置从Dockerfile的指定位置开始使缓存失效。

解决方法

在命令的后面添加注释来使缓存失效。

讨论

例如我们在以下的Dockerfile中的CMD命令后添加注释以让缓存从这里失效:

  1. FROM node
  2. MAINTAINER [email protected]
  3. RUN git clone https://github.com/docker-in-practice/todo.git
  4. WORKDIR todo
  5. RUN npm install
  6. RUN chmod -R 777 /todo
  7. EXPOSE 8000
  8. CMD ["npm","start"] #bust the cache

输出为:

  1. $ docker build .
  2. Sending build context to Docker daemon  2.56 kB
  3. Sending build context to Docker daemon
  4. Step 0 : FROM node
  5.  —> 91cbcf796c2c
  6. Step 1 : MAINTAINER [email protected]
  7.  —> Using cache
  8. A “normal” docker build
  9.    —> 8f5a8a3d9240
  10. Step 2 : RUN git clone -q https://github.com/docker-in-practice/todo.git
  11.  —> Using cache
  12.  —> 48db97331aa2
  13. Step 3 : WORKDIR todo
  14.  —> Using cache
  15.  —> c5c85db751d6
  16. Step 4 : RUN npm install
  17.  —> Using cache
  18.  —> be943c45c55b
  19. Step 5 : EXPOSE 8000
  20.  —> Using cache
  21.  —> 805b18d28a65
  22. Step 6 : CMD ["npm","start"] #bust the cache
  23.  —> Running in fc6c4cd487ce
  24.  —> d66d9572115e
  25. Removing intermediate container fc6c4cd487ce
  26. Successfully built d66d9572115e

从输出你会看到在第6步已经不使用缓存了,而之前的步骤仍使用缓存,这既缩短了构建镜像的时间,又能解决缓存可能引起的问题。
这个技巧工作的原因是我们在命令后添加了非空字符,所以Docker认为这是一个新的命令,也就不使用缓存了。
你会好奇如果我们更改的是第4步命令RUN npm install,那后面的第5步第6步还会使用缓存吗?答案是从更改的第4步开始下面的就不能使用缓存了。所以由于这个原因,建议尽可能的把一些不常需要变更的命令往上移。

Docker实践(25) – 不使用缓存重建镜像

使用Dockerfile构建镜像可以利用它的缓存功能:只有在命令已更改的情况下,才会重建已构建的步骤。下面是重新构建之前涉及到的to-do app的示例:

  1. $ docker build .
  2. Sending build context to Docker daemon  2.56 kB
  3. Sending build context to Docker daemon
  4. Step 0 : FROM node
  5.   —> 91cbcf796c2c
  6. Step 1 : MAINTAINER [email protected]
  7.  —> Using cache
  8. Indicates you’re using the cache
  9. Specifies the cached image/layer ID
  10.    —> 8f5a8a3d9240
  11. Step 2 : RUN git clone -q https://github.com/docker-in-practice/todo.git
  12.  —> Using cache
  13.  —> 48db97331aa2
  14. Step 3 : WORKDIR todo
  15.  —> Using cache
  16.  —> c5c85db751d6
  17. Step 4 : RUN npm install > /dev/null
  18.  —> Using cache
  19.  —> be943c45c55b
  20. Step 5 : EXPOSE 8000
  21.  —> Using cache
  22.  —> 805b18d28a65
  23. Step 6 : CMD npm start
  24.  —> Using cache
  25.  —> 19525d4ec794
  26. Successfully built 19525d4ec794

缓存非常有用并且省时间,不过有时候docker缓存的行为不都能达到你的期望。
用以上Dockerfile作为示例,假设你更改了代码并push到Git仓库。新代码不会check out下来,因为git clone命令没有更改。在Docker看来git clone的步骤一样,所以使用了缓存。
在这种情况下,你可能不想开启docker的缓存了。

问题

你想不用缓存重建Dockerfile。

解决方法

构建镜像时使用–no-cache参数。

讨论

为了强制docker构建镜像时不用缓存,执行带–no-cache参数的docker build命令。下面的示例是使用了–no-cache构建镜像。

  1. $ docker build –no-cache .
  2. Sending build context to Docker daemon  2.56 kB
  3. Sending build context to Docker daemon
  4. Step 0 : FROM node
  5.  —> 91cbcf796c2c
  6. Step 1 : MAINTAINER [email protected]
  7.  —> Running in ca243b77f6a1
  8.  —> 602f1294d7f1
  9. Removing intermediate container ca243b77f6a1
  10. Step 2 : RUN git clone -q https://github.com/docker-in-practice/todo.git
  11.  —> Running in f2c0ac021247
  12.  —> 04ee24faaf18
  13. Removing intermediate container f2c0ac021247
  14. Step 3 : WORKDIR todo
  15.  —> Running in c2d9cd32c182
  16.  —> 4e0029de9074
  17. Removing intermediate container c2d9cd32c182
  18. Step 4 : RUN npm install > /dev/null
  19.  —> Running in 79122dbf9e52
  20. npm WARN package.json [email protected] No repository field.
  21.  —> 9b6531f2036a
  22. Removing intermediate container 79122dbf9e52
  23. Step 5 : EXPOSE 8000
  24.  —> Running in d1d58e1c4b15
  25.  —> f7c1b9151108
  26. Removing intermediate container d1d58e1c4b15
  27. Step 6 : CMD npm start
  28.  —> Running in 697713ebb185
  29.  —> 74f9ad384859
  30. Removing intermediate container 697713ebb185
  31. Successfully built 74f9ad384859

以上的构建镜像步骤没有使用到缓存,每一层的镜像ID都与之间的不同。

Docker实践(24) – 使用ADD命令添加文件到镜像

虽然在Dockerfile内能使用RUN命令或者shell命令来添加文件到镜像,不过这可能很快变得难以管理。Dockerfile命令之一的ADD命令设计用来满足将大量文件放入镜像的需求。

问题

你想以一个简单的方法下载和解压一个tarball文件到你的镜像。

解决方法

tar打包和压缩你的文件,并在你的Dockerfile使用ADD指令。

讨论

使用mkdir add_example && cd add_example为Docker构建镜像准备一个新的环境。然后获取tarball文件:

  1. $ curl https://www.flamingspork.com/projects/libeatmydata/libeatmydata-105.tar.gz >  my.tar.gz

创建Dockerfile:

  1. FROM debian
  2. RUN mkdir -p /opt/libeatmydata
  3. ADD my.tar.gz /opt/libeatmydata/
  4. RUN ls -lRt /opt/libeatmydata

使用docker build –no-cache构建Dockerfile,输出类似如下:

  1. $ docker build –no-cache .
  2. Sending build context to Docker daemon 422.9 kB
  3. Sending build context to Docker daemon
  4. Step 0 : FROM debian
  5.  —> c90d655b99b2
  6. Step 1 : RUN mkdir -p /opt/libeatmydata
  7.  —> Running in fe04bac7df74
  8.  —> c0ab8c88bb46
  9. Removing intermediate container fe04bac7df74
  10. Step 2 : ADD my.tar.gz /opt/libeatmydata/
  11.  —> 06dcd7a88eb7
  12. Removing intermediate container 3f093a1f9e33
  13. Step 3 : RUN ls -lRt /opt/libeatmydata
  14.  —> Running in e3283848ad65
  15. /opt/libeatmydata:
  16. total 4
  17. drwxr-xr-x 7 1000 1000 4096 Oct 29 23:02 libeatmydata-105
  18. /opt/libeatmydata/libeatmydata-105:
  19. total 880
  20. drwxr-xr-x 2 1000 1000  4096 Oct  29 23:02 config
  21. drwxr-xr-x 3 1000 1000  4096 Oct  29 23:02 debian
  22. drwxr-xr-x 2 1000 1000  4096 Oct  29 23:02 docs
  23. drwxr-xr-x 3 1000 1000  4096 Oct  29 23:02 libeatmydata
  24. drwxr-xr-x 2 1000 1000  4096 Oct  29 23:02 m4
  25. -rw-r–r– 1 1000 1000  4096 Oct  29 23:01 config.h.in
  26. […edited…]
  27. -rw-r–r– 1 1000 1000   1824 Jun 18  2012 pandora_have_better_malloc.m4
  28. -rw-r–r– 1 1000 1000    742 Jun 18  2012 pandora_header_assert.m4
  29. -rw-r–r– 1 1000 1000    431 Jun 18  2012 pandora_version.m4
  30.  —> 2ee9b4c8059f
  31. Removing intermediate container e3283848ad65
  32. Successfully built 2ee9b4c8059f

你从输出看到tarball文件已经被Docker daemon解压到了目标目录。Docker支持大多数类型的压缩文件,如.gz,.bz2,.xz,.tar。
值得注意的是如果你在Dockerfile直接从url下载tarball文件,那么它们是不会自动解压的,Docker daemon只解压使用ADD命令添加本地的压缩文件。
如果你使用如下的Dockerfile再次构建镜像,你会发现文件只下载没有解压:

  1. FROM debian The file is retrieved from
  2. RUN mkdir -p /opt/libeatmydata the internet using a URL.
  3. ADD https://www.flamingspork.com/projects/libeatmydata/libeatmydata-105.tar.gz /opt/libeatmydata/
  4. RUN ls -lRt /opt/libeatmydat

下面是输出:

  1. Sending build context to Docker daemon 422.9 kB
  2. Sending build context to Docker daemon
  3. Step 0 : FROM debian
  4.  —> c90d655b99b2
  5. Step 1 : RUN mkdir -p /opt/libeatmydata
  6.  —> Running in 6ac454c52962
  7. —> bdd948e413c1
  8. Removing intermediate container 6ac454c52962
  9. Step 2 : ADD  https://www.flamingspork.com/projects/libeatmydata/libeatmydata-105.tar.gz  /opt/libeatmydata/
  10. Downloading [==================================================>]
  11. 419.4 kB/419.4 kB
  12.  —> 9d8758e90b64
  13. Removing intermediate container 02545663f13f
  14. Step 3 : RUN ls -lRt /opt/libeatmydata
  15.  —> Running in a947eaa04b8e
  16. /opt/libeatmydata:
  17. total 412
  18. -rw——- 1 root root 419427 Jan  1  1970
  19. libeatmydata-105.tar.gz
  20.  —> f18886c2418a
  21. Removing intermediate container a947eaa04b8e
  22. Successfully built f18886c2418a

如果你想添加一个压缩文件到镜像而不想解压,你可以使用COPY命令,这个命令与ADD命令类似,区别就是COPY命令不解压文件。

Docker实践(23) – 找出容器IP

虽然Docker命令让你能够访问有关镜像和容器的信息,但有时你想要了解这些Docker对象的内部元数据,如IP地址。

问题

你想找出容器IP地址。

解决方法

使用docker inspect命令获取和过滤容器元数据。

讨论

通过docker inspect命令能得到JSON格式的docker内部元数据。这会得到很多数据,所以下面只列出一部分。
镜像的原始数据:

  1. $ docker inspect ubuntu | head
  2. [{
  3.     "Architecture": "amd64",
  4.     "Author": "",
  5.     "Comment": "",
  6.     "Config": {
  7.         "AttachStderr": false,
  8.         "AttachStdin": false,
  9.         "AttachStdout": false,
  10.         "Cmd": [
  11.             "/bin/bash"
  12. $

你可以通过名称或ID来查看镜像和容器的信息。当然它们的元数据会有所不同 – 比如容器会有“state”字段,而镜像没有。
你可以通过docker inspect命令,带一个format参数来找出容器的IP:

  1. docker inspect –format ‘{{.NetworkSettings.IPAddress}}’

这个命令对于自动化可能会比较有用,而且这个命令获取的IP比通过其它docker命令要可靠得多。下面的命令是获取所有运行的容器的IP并尝试ping它们。

  1. $ docker ps -q | xargs docker inspect –format='{{.NetworkSettings.IPAddress}}’ | xargs -l1 ping -c1
  2. PING 172.17.0.5 (172.17.0.5) 56(84) bytes of data.
  3. 64 bytes from 172.17.0.5: icmp_seq=1 ttl=64 time=0.095 ms
  4. — 172.17.0.5 ping statistics —
  5. 1 packets transmitted, 1 received, 0% packet loss, time 0ms
  6. rtt min/avg/max/mdev = 0.095/0.095/0.095/0.000 ms

Docker实践(22) – 通过NFS共享数据

在部分公司已经在使用NFS共享文件了。如果应用部署在docker容器里,如果能访问这些共享的文件呢?
docker不支持NFS开箱即用,而在每个容器中安装NFS客户端以便能挂载远程目录不是一个好方法。建议的方法是使用一个docker容器充当其它容器访问NFS共享数据的代理。

问题

你想无缝访问NFS的远程文件系统

解决方法

使用数据容器代理访问

讨论

数据容器的相对介绍请参考http://devops.webres.wang/2016/11/docker-data-container/。下图显示这种方法如何工作。
虚拟化技术
NFS服务器暴露内部目录为/export,此目录与/opt/test/db绑定挂载。然后docker主机使用NFS协议挂载到目录/mnt。然后数据容器挂载主机的/mnt来代理其它容器对共享目录的访问。
虽然NFS的介绍超过了本文的范畴,但NFS与本文的主题密切相关,还是有必要说明下。
假设你想共享系统为Ubuntu14.04的/opt/test/db目录给其它主机的容器。
切换为root权限,安装NFS服务器并创建一个export目录:

  1. # apt-get install nfs-kernel-server
  2. # mkdir /export
  3. # chmod 777 /export

现在绑定挂载db目录到/export目录:

  1. # mount –bind /opt/test/db /export

你现在应该能在/export目录访问/opt/test/db的内容。

如果你想让重启后绑定还生效,添加/opt/test/db /export none bind 0 0到/etc/fstab文件。

现在添加如下行到/etc/exports文件:

  1. /export 127.0.0.1(ro,fsid=0,insecure,no_subtree_check,async)

因为我们只用了一台机器做试验,即nfs服务器与容器都在一台,所以这里埴写了127.0.0.1,如果是在其它机器,记得把127.0.0.1替换为nfs客户端IP。
考虑到安全,我们在这里只读挂载,不过你也可以把ro替换为rw设置读写挂载,如果这样做,记得在async选项后添加no_root_squash。
然后通过NFS挂载/export目录到/mnt目录:

  1. # mount -t nfs 127.0.0.1:/export /mnt
  2. # exportfs -a
  3. # service nfs-kernel-server restart

现在运行一个数据容器:

  1. # docker run -ti –name nfs_client –privileged -v /mnt:/mnt busybox /bin/true

然后其它容器关联此数据容器的volumes:

  1. # docker run -ti –volumes-from nfs_client debian /bin/bash
  2. root@079d70f79d84:/# ls /mnt
  3. myb
  4. root@079d70f79d84:/# cd /mnt
  5. root@079d70f79d84:/mnt# touch asd
  6. touch: cannot touch `asd’: Read-only file system

Docker实践(21) – 使用sshfs挂载远程volume

我们已经讨论过如何挂载本地文件,不过很快我们就会遇到如何挂载远程文件系统的问题了。例如可能你想共享远程服务器上的数据库并把它当作本地数据库使用。
虽然理论上可以设置一个NFS服务器并通过在你服务器挂载目录访问远程文件,不过有一种对于大多数用户快速简单的方法,此方法不需要在服务器端安装任何软件(只需要SSH)。

设置sshfs需要root权限,并且需要安装FUSE(Linux的“用户空间文件系统”内核模块)。你可以通过执行ls /dev/fuse来判断系统是否已经安装有。

问题

你想找到一个不需要配置服务器端的方法来挂载远程的文件系统。

解决方法

使用sshfs来挂载远程文件系统

讨论

sshfs通过使用FUSE内核模块提供的文件系统标准接口和SSH访问远程文件系统,所有的通信使用SSH进行。SSHFS还提供了各种幕后功能(例如远程文件预读),以让我们觉得是在操作本地文件。下图说明它是如何工作的。
虚拟化技术
我们现在来做下试验。首先在你主机运行一个带–privileged参数的容器:

  1. $ docker run -t -i –privileged debian /bin/bash

然后当它启动后,在容器内部运行apt-get update && apt-get install sshfs安装SSHFS。
当SSHFS安装完成后,按如何登录远程主机:

  1. $ LOCALPATH=/path/to/local/directory
  2. $ mkdir $LOCALPATH
  3. $ sshfs user@host:/path/to/remote/directory $LOCALPATH

现在你应该能通过本地的$LOCALPATH目录看到远程主机的目录内容了。
不需要时可以通过fusermount命令来取消挂载:

  1. fusermount -u /path/to/local/directory

Docker实践(20) – 数据容器

如果你在主机上使用很多volumes,管理容器的启动可能会变得棘手。你可能希望数据只由容器管理,不管在主机上能直接访问数据。一种简单的方法是使用data-only容器。

问题

你想在容器内部访问外部volume,不过你希望这些volume只能由docker访问。

解决方法

设置一个数据容器并在其它容器运行时使用–volumes-from参数连接此数据容器。

讨论

下图展示了数据容器模式的结构,并解释了它的工作原理。
虚拟化技术
最主要是注意第二个主机,容器不需要知道数据在硬盘的哪个位置。它们所要知道的是数据容器的名称。这使得容器的操作更便捷。
这种方法比直接映射主机目录的另一个好处是,只能由docker访问这些文件,其它进程无法访问。

数据容器不需要运行! 一个常见的困惑是是否需要运行数据容器。不需要!仅仅需要数据容器在主机运行过并不被删除,之后就不需要运行了,但要保存它一直存在。

我们看一个示例看看数据容器怎么使用。
首先运行一个数据容器:

  1. $ docker run -v /shared-data –name dc busybox touch /shared-data/somefile

-v参数不映射主机上的目录,它只在容器责任范围内创建一个目录。此目录使用touch创建了一个文件,这样数据容器马上生效了 – 数据容器不需要运行使用。我们使用busybox镜像来减小容器大小。
然后运行另一个容器来连接数据容器:

  1. docker run -t -i –volumes-from dc busybox /bin/sh
  2. / # ls /shared-data
  3. somefile

–volumes-from参数允许你关联数据容器的volume到当前容器的目录 – 你只需要指定数据容器的ID。

volumes永久存在!当最后一个容器取消关联数据容器时,数据容器的数据不会被删除。 这是为了防止不必要的数据丢失。

使用数据容器可能比直接挂载目录管理起来要难,因为数据只能由docker访问,而不能像直接挂载的能在主机上直接访问。不过你如果希望把volumes给一个容器来管理,希望以后变动时不那么麻烦,那么数据容器可能适合你。

如果你的多个容器的应用程序正在往数据容器里写日志,那么确保它们在数据容器里日志的路径不一样很重要。因为如果日志路径一样的话,那么不同的容器可能会覆盖或截断文件,导致数据丢失。

Docker实践(19) – 保留容器的bash历史记录

我们知道在容器内做试验完成时,容器可以设置自动清除,这就省了好多事。不过仍然有一些不方便的地方。其中一个是我们可能需要在容器中多次执行一个相同的命令,而bash历史又没有了,只能手打。

问题

你想与容器共享你的主机bash历史记录。

解决方法

为docker run命令设置一个别名来与主机共享bash历史记录。

讨论

要理解这个问题,我们来展示一个场景。
假设你在Docker容器做试验,比如做一些有趣的和可重复使用的东西。这里我们使用echo命令,但这可能是一个长且复杂的一连串程序:

  1. $ docker run -ti –rm ubuntu /bin/bash
  2. $ echo my amazing command
  3. $ exit

过了一些时间,你想重新执行之间执行过的复杂的echo命令。不过,你不记得命令了,而且终端会话也找不到这个命令,你尝试找bash历史记录,但没有任何输出:

  1. $ history | grep amazing

为了解决这个问题,我们可以在你运行docker镜像时挂载一个volume,如下:

  1. $ docker run -e HIST_FILE=/root/.bash_history
  2.  -v=$HOME/.bash_history:/root/.bash_history
  3.  -ti ubuntu /bin/bash

命令太长太难记了,我们把它设置为一个别名吧,在~/.bashrc文件中添加:

  1. $ alias dockbash=’docker run -e HIST_FILE=/root/.bash_history
  2.  -v=$HOME/.bash_history:/root/.bash_history

这个体验仍然不好,因为当你要执行docker run命令时都要记住执行dockerbash命令。要获得更加无缝的体验,你您可以将这些添加到〜/ .bashrc文件:

  1. function basher() {
  2.   if [[ $1 = ‘run’ ]]
  3.   then
  4. shift
  5. /usr/bin/docker run
  6.   -e HIST_FILE=/root/.bash_history
  7.   -v $HOME/.bash_history:/root/.bash_history "$@"
  8.    else
  9.     /usr/bin/docker "$@"
  10. fi
  11. }alias docker=basher

现在当你打开bash shel并执行任意的docker run命令时都会自动把bash历史记录的设置添加到命令中去。

Docker实践(18) – 使用BitTorrent Sync分发volumes

在团队中试用Docker时,你可能希望能够在团队成员之间共享大量数据,不过你可能没有足够的空间来搭建一台共享服务器。最简单的方法是当你需要时再从其它团队成员复制最新的文件 – 不过在一个更大的团队中这个方法就不太可行了。
解决方案是使用BitTorrent Sync工具来共享文件 – 不需要专门的资源。

问题

你想通过internet在主机之间共享volumes。

解决方法

使用BitTorrent Sync镜像来共享volume。

讨论

下图说明了这个方法是如何运作的。
虚拟化技术
最终结果是通过因特网方便地同步的卷(/data),不需要任何复杂的设置。
执行如下命令在第一台主机上设置容器:

  1. [host1]$ docker run -d -p 8888:8888 -p 55555:55555 –name btsync ctlc/btsync
  2. $ docker logs btsync
  3. Starting btsync with secret: ALSVEUABQQ5ILRS2OQJKAOKCU5SIIP6A3
  4. By using this application, you agree to our Privacy Policy and Terms.
  5. http://www.bittorrent.com/legal/privacy
  6. http://www.bittorrent.com/legal/terms-of-use
  7. total physical memory 536870912 max disk cache 2097152
  8. Using IP address 172.17.4.121
  1. [host1]$ docker run -i -t –volumes-from btsync ubuntu /bin/bash
  2. $ touch /data/shared_from_server_one
  3. $ ls /data
  4. shared_from_server_one

在第二个服务器,打开一个终端运行这些命令来同步volume:

  1. [host2]$ docker run -d –name btsync-client -p 8888:8888 -p 55555:55555 ctlc/btsync ALSVEUABQQ5ILRS2OQJKAOKCU5SIIP6A3
  1. [host2]$ docker run -i -t –volumes-from btsync-client ubuntu bash
  2. $ ls /data
  3. shared_from_server_one
  4. $ touch /data/shared_from_server_two
  5. $ ls /data
  6. shared_from_server_one  shared_from_server_two

回到第一台主机运行的容器,你应该能看到第二台主机的创建的文件了:

  1. [host1]$ ls /data
  2. shared_from_server_one  shared_from_server_two

Docker实践(17) – 容器挂载主机目录

容器最强大的功能是它在迁移时能保持系统环境一致性。
不过有时候你不想把所有的文件放进容器中。你可能想在容器之间共享一些大的文件,或者单独管理这些文件。典型的例子是你希望容器访问大型集中式数据库,但是还希望其它客户端也能与容器一起访问。
解决方法是volumes,在容器生命周期外管理文件的Docker机制。虽然这违背了容器“部署在任何地方”的理念(例如,你将无法在没有挂载数据库的系统中部署容器),不过有时候在实际环境中不得不用。

问题

你想在容器内部访问主机的文件。

解决方法

使用Docker的volume参数来设置容器访问主机文件。

讨论

下面的命令显示主机的/var/db/tables目录被挂载到容器的/var/data1上。

  1. $ docker run -v /var/db/tables:/var/data1 -it debian bash

-v参数(–volume)表明需要为容器设置一个外部的volume。随后的参数值是由两个以冒号分隔的目录组成,冒号之前的目录是主机目录,之后是容器目录,如果这两个目录不存在则会自动创建。
下图说明容器访问主机目录是如何交互的:
虚拟化技术