5. OnBuild优化Dockerfile
虽然Dockerfile是按从上到下的顺序执行的,但当该映像用作另一个映像的基础时,您可以触发一条指令在稍后的时间执行。
ONBUILD指令可以为镜像添加触发器。
当我们在一个Dockerfile文件中加上ONBUILD指令,该指令对利用该Dockerfile构建镜像(比如为A镜像)不会产生实质性影响。
但是当我们编写一个新的Dockerfile文件来基于A镜像构建一个镜像(比如为B镜像)时,这时构造A镜像的Dockerfile文件中的ONBUILD指令就生效了,在构建B镜像的过程中,首先会执行ONBUILD指令指定的指令,然后才会执行其它指令。
需要注意的是,如果是再利用B镜像构造新的镜像时,那个ONBUILD指令就无效了,也就是说只能再构建子镜像中执行,对孙子镜像构建无效。其实想想是合理的,因为在构建子镜像中已经执行了,如果孙子镜像构建还要执行,相当于重复执行,这就有问题了。
利用ONBUILD指令,实际上就是相当于创建一个模板镜像,后续可以根据该模板镜像创建特定的子镜像,需要在子镜像构建过程中执行的一些通用操作就可以在模板镜像对应的dockerfile文件中用ONBUILD指令指定。 从而减少dockerfile文件的重复内容编写。
下面是Node.js的OnBuild Dockerfile。与前面的场景不同,应用程序指定命令以ONBUILD作为前缀。
Dockerfile1:
FROM node:7 RUN mkdir -p /usr/src/app WORKDIR /usr/src/app ONBUILD COPY package.json /usr/src/app/ ONBUILD RUN npm install ONBUILD COPY . /usr/src/app CMD [ "npm", "start" ]
结果是,我们可以构建这个映像,但在将构建的映像用作基本映像之前,不会执行应用程序特定的命令。然后,它们将作为基本映像构建的一部分执行。
有了复制代码、安装依赖项和启动应用程序的所有逻辑后,在应用程序级别上需要定义的唯一方面就是要公开哪个端口。
创建OnBuild映像的好处是,我们的Dockerfile现在更简单,可以轻松地跨多个项目重用,而不必重新运行相同的步骤,以提高构建时间。
dockerfile2:
FROM node:7-onbuild EXPOSE 3000
已经为您创建了上一步中的Dockerfile。基于OnBuild docker
文件构建映像与之前相同。OnBuild命令将像在基础Dockerfile中一样执行。
总之,唯有 ONBUILD
是为了帮助别人定制自己而准备的。而不是为了构建当前镜像的。
docker build -t my-nodejs-app -f dockerfile2 docker run -d --name my-running-app -p 3000:3000 my-nodejs-app curl http://ip:3000
6. 忽略文件.dockerignore
6.1 Docker Ignore
为了防止敏感文件或目录被错误地包含在映像中,您可以添加一个名为.dockerignore
的文件。
Dockerfile将工作目录复制到Docker映像中。因此,这将包括潜在的敏感信息,如我们希望在映像外部管理的密码文件。
$ ls Dockerfile cmd.sh passwords.txt $ cat Dockerfile FROM alpine ADD . /app COPY cmd.sh /cmd.sh CMD ["sh", "-c", "/cmd.sh"] $ cat cmd.sh echo "Hello World" $ cat passwords.txt admin:admin $ docker build -t password . Sending build context to Docker daemon 4.096kB Step 1/4 : FROM alpine ---> 3fd9065eaf02 Step 2/4 : ADD . /app ---> 8e7bc5dac978 Step 3/4 : COPY cmd.sh /cmd.sh ---> ec486638d561 Step 4/4 : CMD ["sh", "-c", "/cmd.sh"] ---> Running in fe4cba7a87b2 Removing intermediate container fe4cba7a87b2 ---> 4c270e87d27c Successfully built 4c270e87d27c Successfully tagged password:latest $ docker run password ls /app Dockerfile cmd.sh passwords.txt
这将包括密码文件。
下面的命令将在.dockerignore
文件中包含password .txt
,并确保它不会意外地出现在容器中。dockerignore文件将存储在源代码管理中,并与团队共享,以确保每个人都是一致的。
echo passwords.txt >> .dockerignore
.dockerignore
文件支持目录和正则表达式来定义限制,非常类似于.gitignore
。这个文件还可以用来提高构建时间,我们将在下一步研究这个问题。
构建映像,因为Docker Ignore文件不应该包括密码文件。
$ docker build -t nopassword . Sending build context to Docker daemon 4.096kB Step 1/4 : FROM alpine ---> 3fd9065eaf02 Step 2/4 : ADD . /app ---> 36ee8b3bc4ee Step 3/4 : COPY cmd.sh /cmd.sh ---> a4c8fc953352 Step 4/4 : CMD ["sh", "-c", "/cmd.sh"] ---> Running in 5b7774763eca Removing intermediate container 5b7774763eca ---> b6b6eac92cce Successfully built b6b6eac92cce Successfully tagged nopassword:latest $ docker run nopassword ls /app Dockerfile cmd.sh
如果您需要使用密码作为RUN命令的一部分,那么您需要复制、执行和删除文件作为单个RUN命令的一部分。只有Docker容器的最终状态被持久化到映像中。
6.2 Docker 构建安全上下文
dockerignore文件可以确保Docker映像中不包含敏感细节。但是,它们也可以用来提高映像的构建时间。
在环境中,已经创建了100M的临时文件。Dockerfile永远不会使用这个文件。当您执行构建命令时,Docker将整个路径内容发送给引擎,以便它计算要包含哪些文件。因此,发送100M文件是不需要的,并创建了一个较慢的构建。
您可以通过执行该命令看到100M的影响。
$ docker build -t large-file-context . Sending build context to Docker daemon 104.9MB Step 1/4 : FROM alpine ---> 3fd9065eaf02 Step 2/4 : ADD . /app ---> cb1e74c524af Step 3/4 : COPY cmd.sh /cmd.sh ---> e3dbbbd57ddf Step 4/4 : CMD ["sh", "-c", "/cmd.sh"] ---> Running in 5fcb5e771266 Removing intermediate container 5fcb5e771266 ---> 7e398a079fb0 Successfully built 7e398a079fb0 Successfully tagged large-file-context:latest
在下一步中,我们将演示如何提高构建的性能。
明智的做法是忽略.git
目录以及在映像中下载/构建的依赖项,比如node_modules
。在Docker容器中运行的应用程序永远不会使用它们,只会增加构建过程的开销。
6.3 优化构建
以同样的方式,我们使用.dockerignore文件来排除敏感文件,我们可以使用它来排除我们不想在构建期间发送到Docker构建上下文的文件。
要加快构建速度,只需在忽略文件中包含大文件的文件名。
echo big-temp-file.img >> .dockerignore
当我们重建图像时,它将会快得多,因为它不需要复制100M文件。
$ docker build -t no-large-file-context . Sending build context to Docker daemon 4.096kB Step 1/4 : FROM alpine ---> 3fd9065eaf02 Step 2/4 : ADD . /app ---> Using cache ---> 4a1be3423c29 Step 3/4 : COPY cmd.sh /cmd.sh ---> Using cache ---> e30db2162cca Step 4/4 : CMD ["sh", "-c", "/cmd.sh"] ---> Using cache ---> 4d4964ddbb00 Successfully built 4d4964ddbb00 Successfully tagged no-large-file-context:latest
当忽略.git
这样的大目录时,这种优化会产生更大的影响。
7. 容器持久化数据
数据容器是唯一负责存储/管理数据的容器。
与其他容器一样,它们由主机系统管理。然而,当您执行docker ps命令时,它们不会运行。
要创建数据容器,我们首先要创建一个具有知名名称的容器以供将来参考。我们使用busybox作为基础,因为它体积小,重量轻,以防我们想要探索和移动容器到另一个主机。
在创建容器时,我们还提供了一个-v选项来定义其他容器读取/保存数据的位置。
$ docker create -v /config --name dataContainer busybox
容器就绪后,我们现在可以将文件从本地客户端目录复制到容器中。
下面的命令将config.conf文件复制到dataContainer和config.conf
目录中。
$ docker cp config.conf dataContainer:/config/
现在我们的Data Container有了配置,我们可以在启动需要配置文件的依赖容器时引用该容器。
使用——volumes-from <container>选项,我们可以使用正在启动的容器中来自其他容器的挂载卷。在这种情况下,我们将启动一个Ubuntu容器,它引用了我们的数据容器。当我们列出config目录时,它将显示来自附加容器的文件。
$ docker run --volumes-from dataContainer ubuntu ls /config config.conf
如果/config
目录已经存在,那么volumes-from
将被覆盖并成为所使用的目录。可以将多个卷映射到一个容器。
如果我们想将Data Container移动到另一台机器,那么我们可以将其导出到.tar文件。
$ docker export dataContainer > dataContainer.tar
命令docker import dataContainer.tar
会将数据容器导入到docker中。
8. 容器之间的交流
连接到容器最常见的场景是应用程序连接到数据存储。创建链接时的关键方面是容器的名称。所有容器都有名称,但为了在处理链接时更容易一些,为所连接的源容器定义一个友好的名称是很重要的。
运行一个友好的名称为redis-server的redis服务器,我们将在下一步连接它。这将是源容器。
$ docker run -d --name redis-server redis
Redis是一个快速的、开源的键值数据存储。
要连接到源容器,在启动新容器时使用--link <container-name|id>:<alias>选项。容器名引用上一步中定义的源容器,而别名定义主机的友好名称。
通过设置别名,我们可以将应用程序的配置方式与基础设施的调用方式分开。这意味着应用程序配置在连接到其他环境时不需要更改。
在这个例子中,我们打开一个链接到redis-server的Alpine容器。我们已经将别名定义为redis。当一个链接被创建时,Docker将做两件事。
首先,Docker将基于链接到容器的环境变量设置一些环境变量。这些环境变量为您提供了一种通过已知名称引用端口和IP地址等信息的方法。
可以使用env命令输出所有环境变量。例如:
$ docker run --link redis-server:redis alpine env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=bae49bf11c01 REDIS_PORT=tcp://172.18.0.2:6379 REDIS_PORT_6379_TCP=tcp://172.18.0.2:6379 REDIS_PORT_6379_TCP_ADDR=172.18.0.2 REDIS_PORT_6379_TCP_PORT=6379 REDIS_PORT_6379_TCP_PROTO=tcp REDIS_NAME=/angry_franklin/redis REDIS_ENV_GOSU_VERSION=1.12 REDIS_ENV_REDIS_VERSION=6.2.5 REDIS_ENV_REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-6.2.5.tar.gz REDIS_ENV_REDIS_DOWNLOAD_SHA=4b9a75709a1b74b3785e20a6c158cab94cf52298aa381eea947a678a60d551ae HOME=/root
通过创建链接,您可以以与在您的网络中运行的服务器相同的方式ping源容器。
$ docker run --link redis-server:redis alpine ping -c 1 redis PING redis (172.18.0.2): 56 data bytes 64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.202 ms --- redis ping statistics --- 1 packets transmitted, 1 packets received, 0% packet loss round-trip min/avg/max = 0.202/0.202/0.202 ms
通过创建链接,应用程序可以以通常的方式与源容器进行连接和通信,而无需考虑两个服务都运行在容器中这一事实。
这是一个简单的node.js应用程序,它使用主机名redis连接到redis。
$ docker run -d -p 3000:3000 --link redis-server:redis katacoda/redis-node-docker-example
发送一个HTTP请求到应用程序将存储请求在Redis和返回一个计数。如果发出多个请求,就会看到计数器的递增,因为条目被持久化了。
$ curl ip:3000 This page was generated after talking to redis. Application Build: 1 Total requests: 1 IP count: ::ffff:172.17.0.33: 1 $ curl ip:3000 This page was generated after talking to redis. Application Build: 1 Total requests: 2 IP count: ::ffff:172.17.0.33: 2
以同样的方式,您可以将源容器连接到应用程序,也可以将它们连接到自己的CLI工具。
下面的命令将启动一个redis -cli工具的实例,并通过它的别名连接到redis服务器。
$ docker run -it --link redis-server:redis redis redis-cli -h redis redis:6379> info
KEYS *
命令将输出当前存储在源redis容器中的内容。
9. Docker 网络
9.1 创建网络
第一步是使用CLI创建网络。这个网络将允许我们附加多个容器,这些容器将能够发现彼此。在本例中,我们将从创建一个后端网络开始。所有连接到我们后端的容器都将在这个网络上。
$ docker network create backend-network 89109e8de51aee15171ac6ec7257af040aecc66906777acfbbe88a715dcdb9d4
当我们启动新的容器时,我们可以使用--net
属性来分配它们应该连接到哪个网络。
$ docker run -d --name=redis --net=backend-network redis
9.2 连接网络
与使用链接不同,docker网络的行为类似于传统网络,节点可以附加/分离。
首先你会注意到Docker不再分配环境变量或更新容器的hosts文件。使用下面的两个命令,您会注意到它不再提到其他容器。
$ docker run --net=backend-network alpine env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=d566ff9c9a14 HOME=/root $ docker run --net=backend-network alpine cat /etc/hosts 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters 172.19.0.3 97c3a236a7e6
相反,容器可以通过Docker中的嵌入式DNS服务器进行通信。这个DNS服务器通过IP 127.0.0.11
分配给所有容器,并在resolv.conf
文件中设置。
$ docker run --net=backend-network alpine cat /etc/resolv.conf nameserver 127.0.0.11 options ndots:0
当容器试图通过众所周知的名称(如Redis)访问其他容器时,DNS服务器将返回正确的容器的IP地址。在这种情况下,Redis的完全限定名将是Redis .backend-network
。
$ docker run --net=backend-network alpine ping -c1 redis PING redis (172.19.0.2): 56 data bytes 64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.324 ms --- redis ping statistics --- 1 packets transmitted, 1 packets received, 0% packet loss round-trip min/avg/max = 0.324/0.324/0.324 ms
9.3 连接两个容器
Docker支持多个网络和容器同时连接到多个网络。
例如,让我们用Node.js应用程序创建一个单独的网络,它与我们现有的Redis实例通信。
第一个任务是以同样的方式创建一个新的网络。
$ docker network create frontend-network 37e9702dd8f695f515b988beddd1cf4d4f7b38447a4e4177d98fcf96231321b2
当使用connect命令时,可以将现有容器附加到网络上。
$ docker network connect frontend-network redis
当我们启动web服务器时,考虑到它连接到同一个网络,它将能够与我们的Redis实例通信。
$ docker run -d -p 3000:3000 --net=frontend-network katacoda/redis-node-docker-example
$ curl ping:3000 This page was generated after talking to redis. Application Build: 1 Total requests: 1 IP count: ::ffff:172.17.0.51: 1 $ curl ping:3000 This page was generated after talking to redis. Application Build: 1 Total requests: 2 IP count: ::ffff:172.17.0.51: 2
9.4 创建别名
使用docker网络时仍然支持链接,并提供了一种方法来定义容器名的别名。这将为容器提供一个额外的DNS条目名称和被发现的方式。当使用--link时,嵌入式DNS将保证本地化查找结果只在使用--link的容器上。
另一种方法是在将容器连接到网络时提供别名。
下面的命令将用db的别名将我们的Redis实例连接到前端网络。
docker network create frontend-network2 docker network connect --alias db frontend-network2 redis
当容器试图通过名称db访问服务时,他们将得到我们的Redis容器的IP地址。
$ docker run --net=frontend-network2 alpine ping -c1 db PING db (172.21.0.2): 56 data bytes 64 bytes from 172.21.0.2: seq=0 ttl=64 time=0.170 ms --- db ping statistics --- 1 packets transmitted, 1 packets received, 0% packet loss round-trip min/avg/max = 0.170/0.170/0.170 ms
9.5 断开容器连接
创建好网络后,我们可以使用CLI来探索细节。下面的命令将列出我们主机上的所有网络。
$ docker network ls NETWORK ID NAME DRIVER SCOPE 89109e8de51a backend-network bridge local 6fe697227a58 bridge bridge local 37e9702dd8f6 frontend-network bridge local b0a9dbbb0bab frontend-network2 bridge local fa054a9af353 host host local f50397115ef2 none null local
然后,我们可以探索网络,查看连接的容器及其IP地址。
$ docker network inspect frontend-network [ { "Name": "frontend-network", "Id": "37e9702dd8f695f515b988beddd1cf4d4f7b38447a4e4177d98fcf96231321b2", "Created": "2021-09-28T12:52:03.799072129Z", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": {}, "Config": [ { "Subnet": "172.20.0.0/16", "Gateway": "172.20.0.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "80bb046b3ac46cd0efa7b66640e0eaf297f65d39ad080193758f6d19d10c6d3e": { "Name": "redis", "EndpointID": "a8fb389c6672f0ab173c39b273064922ea252d3fce5094d1864fb2b36cdfa25d", "MacAddress": "02:42:ac:14:00:02", "IPv4Address": "172.20.0.2/16", "IPv6Address": "" }, "b28cbfb9b69e68d070f8fef5ddd2bbabb6e410ebeb0905b917dfd8eb103d85a3": { "Name": "inspiring_archimedes", "EndpointID": "80afb91170ab08def612373bf78be60609dbb173bb3d57b3abda7b578ff001cc", "MacAddress": "02:42:ac:14:00:03", "IPv4Address": "172.20.0.3/16", "IPv6Address": "" } }, "Options": {}, "Labels": {} } ]
下面的命令断开redis容器与前端网络的连接。
$ docker network disconnect frontend-network redis
10. 使用卷持久化存储
10.1 --volumes,-v
在启动容器时创建和分配Docker卷。数据卷允许将主机目录映射到容器,以便共享数据。
这种映射是双向的。它允许从容器内部访问存储在主机上的数据。这还意味着进程在容器内保存的数据会持久化到主机上。
这个例子将使用Redis作为一种持久化数据的方法。在下面启动一个Redis容器,并使用-v参数创建一个数据卷。它指定容器中保存到/data目录中的任何数据都应该持久化到主机的/docker/redis-data目录中。
docker run -v /docker/redis-data:/data --name r1 -d redis redis-server --appendonly yes
我们可以使用下面的命令将数据输送到Redis实例中。
$ cat data | docker exec -i r1 redis-cli --pipe All data transferred. Waiting for the last reply... Last reply received from server. errors: 0, replies: 1
Redis会将这些数据保存到磁盘。在主机上,我们可以调查应该包含Redis数据文件的映射直接。
$ ls /docker/redis-data appendonly.aof
这个目录可以挂载到第二个容器。一种用法是让Docker容器对数据执行备份操作。
$ docker run -v /docker/redis-data:/backup ubuntu ls /backup appendonly.aof
10.2 --volumes-from
数据卷映射给主机有利于数据持久化。然而,要从另一个容器访问它们,您需要知道容易出错的确切路径。
另一种方法是使用-volumes-from。该参数将映射卷从源容器映射到正在启动的容器。
在这个例子中,我们将Redis容器的卷映射到Ubuntu容器。/data目录只存在于我们的Redis容器中,然而,因为-volumes-from,Ubuntu容器可以访问数据。
$ docker run --volumes-from r1 -it ubuntu ls /data appendonly.aof
这允许我们访问来自其他容器的卷,而不必关心它们是如何在主机上持久化的。
10.3 只读卷
挂载卷使容器对目录具有完全的读和写访问权限。通过对挂载目录添加“ro”权限,可以对该目录设置只读权限。如果容器试图修改目录中的数据,则会出错。
$ docker run -v /docker/redis-data:/data:ro -it ubuntu rm -rf /data rm: cannot remove '/data/appendonly.aof': Read-only file system
11. 管理日志
当你启动一个容器时,Docker将跟踪进程的Standard Out
和Standard Error
输出,并通过客户端使它们可用。
在后台,有一个名为Redis -server的Redis实例运行。通过使用Docker客户端,我们可以访问标准输出和标准错误输出
$ docker logs redis-server
默认情况下,Docker日志使用JSON -file记录器输出,这意味着输出存储在主机上的JSON文件中。这可能会导致大文件填满磁盘。因此,您可以更改日志驱动程序以移动到不同的目的地。
Syslog日志驱动程序将所有容器日志写到主机的中央Syslog日志中。syslog是一种广泛使用的消息记录标准。它允许生成消息的软件、存储消息的系统以及报告和分析消息的软件分离。
此日志驱动程序设计用于外部系统收集和聚合syslog日志。下面的命令将redis日志重定向到syslog
$ docker run -d --name redis-syslog --log-driver=syslog redis
如果你试图使用客户端查看日志,你会收到错误FATA[0000] "logs"命令只支持"json-file"日志驱动程序。
相反,您需要通过syslog流访问它们。
第三个选项是禁用容器上的日志记录。这对于在日志记录中非常冗长的容器特别有用。
当容器启动时,只需将log-driver设置为none。不会记录任何输出。
docker run -d --name redis-none --log-driver=none redis
inspect命令允许您识别特定容器的日志记录配置。下面的命令将为每个容器输出LogConfig部分。
$ docker inspect --format '{{ .HostConfig.LogConfig }}' redis-server {json-file map[]} $ docker inspect --format '{{ .HostConfig.LogConfig }}' redis-syslog {syslog map[]} $ docker inspect --format '{{ .HostConfig.LogConfig }}' redis-none {none map[]}