原文地址:http://www.eussi.top/view/24
5.2 Dockerfile指令失败处理
当构建指令失败时,我们需要调试。
如果很好看出来错误原因那自然不必多说,修改一下执行命令即可;
如果无法快速定位错误原因,此时可以找到构建过程中最后成功的一次,通过其镜像ID,使用docker run指令运行一个容器,打开一个会话,执行出现问题的指令,查看错误原因。
找到原因后,修改为正确的指令,再次尝试构建即可。
5.3 Dockerfile和构建缓存
由于每一步的构建过程都会将结果提交为镜像,所以 Docker的构建镜像过程就显得非常聪明。它会将之前的镜像层看做缓存。如果再次构建时,针对Dockerfile中的命令,如果之前有相同的构建,则会直接使用之前已有的缓存。
有些时候需要确保构建过程不会使用缓存。要想略过缓存功能,可以使用 docker build的-no- cache标志,如:
docker build --no-cache -t="eussi/test-repo"
5.4 基于构建缓存的 DockerFile模板
构建缓存带来的一个好处就是,我们可以实现简单的 Dockerfile模板(比如在Dockerfile文件顶部增加包仓库或者更新包,从而尽可能确保缓存命中)。可以在自己的Dockerfile文件顶部使用相同的指令集模板,比如对centos所示的模版:
# Version:0.0.1
FROM centos
MAINTAINER Wang Xmeming "wangxuemingcn@yeah.net"
ENV REFRESHED_AT 2019-04-05
RUN yum update
首先,通过FROM指令为新镜像设置了一个基础镜像centos。接着,使用 MAINTAINER指令添加了自己的详细联系信息。之后使用一条新出现的指令ENV来在镜像中设置环境变量。在这个例子里,通过ENV指令来设置了一个名为 REFRESHED_AT的环境变量,这个环境变量用来表明该镜像模板最后的更新时间。最后,我使用了RUN指令来运行yum update命令。有了这个模板,如果想刷新一个构建,只需修改ENV指令中的日期。这使 Docker在命中ENV指令时开始重置这个缓存,并运行后续指令而无须依赖该缓存。也就是说,RUN yum update这条指令将会被再次执行。可以扩展此模板,比如适配到不同的平台或者添加额外的需求。
5.5 查看新镜像
查看镜像:
docker images eussi/test-repo
即
$ docker images eussi/test-repo
REPOSITORY TAG IMAGE ID CREATED SIZE
eussi/test-repo latest 98ab6a8ede3a 12 hours ago 315MB
查看构建历史:
docker history eussi/test-repo
即:
$ docker history eussi/test-repo
IMAGE CREATED CREATED BY SIZE COMMENT
98ab6a8ede3a 12 hours ago /bin/sh -c #(nop) EXPOSE 80 0B
ad9e92a0767b 12 hours ago /bin/sh -c echo 'Hi, I am nginx in docker' >… 25B
dfcb3b042bd0 12 hours ago /bin/sh -c yum install -y nginx 90.4MB
1b5112400c14 12 hours ago /bin/sh -c rpm -ivh http://nginx.org/package… 22.6MB
174c4e10f20a 12 hours ago /bin/sh -c #(nop) MAINTAINER Wang Xmeming "… 0B
9f38484d220f 3 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 3 weeks ago /bin/sh -c #(nop) LABEL org.label-schema.sc… 0B
<missing> 3 weeks ago /bin/sh -c #(nop) ADD file:074f2c974463ab38c… 202MB
5.6 从新镜像启动容器
下面我们基于新镜像启动一个新容器,检查是否构建成功:
docker run -d -p 80 --name nginx-docker eussi/test-repo nginx -g "daemon off;"
在这里,使用docker run命令,基于之前构建的镜像的名字,启动了一个名为nginx-docker的新容器。我们同时指定了-d选项,告诉Docker以分离(detached)的方式在后台运行。这种方式非常适合运行类似 nginx守护进程这样的需要长时间运行的进程。我
们也指定了需要在容器中运行的命令:nginx-g" daemon off;"
。这将以前台运行的方式启动 Nginx,来作为我们的Web服务器。我们这里也使用了一个新的-p标志,该标志用来控制 Docker在运行时应该公开哪些网络端口给外部(宿主机)。运行一个容器时,Docker可以通过两种方法来在宿主机上分配端口
- Docker可以在宿主机上随机选择一个位于49153-65535的一个比较大的端口号来映射到容器中的80端口上。
- 可以在 Docker宿主机中指定一个具体的端口号来映射到容器中的80端口上。
这将在Docker宿主机上随机打开一个端口,这个端口会连接到容器中的80端口上。使用 docker ps命令来看一下容器的端口分配情况:
$ docker ps -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
260789a53330 eussi/test-repo "nginx -g 'daemon of…" 13 minutes ago Up 13 minutes 0.0.0.0:32768->80/tcp nginx-docker
可以看到docker 映射端口情况,也可以通过docker port查看端口映射情况:
$ docker port nginx-docker 80
0.0.0.0:32768
-p选项还让我们可以灵活地管理容器和宿主机之间的端口映射关系。比如,可以指定将容器中的端口映射到 Docker宿主机的某一特定端口上,如:
docker run -d -p 80:80 --name nginx-docker eussi/test-repo nginx -g "daemon off;"
上面的命令会将容器内的80端口绑定到本地宿主机的80端口上。很明显,我们必须非常小心地使用这种直接绑定的做法:如果要运行多个容器,只有一个容器能成功地将端口绑定到本地宿主机上。这将会限制 Docker的灵活性。为了避免这个问题,我们可以将容器内的端口绑定到不同的宿主机端口。
docker run -d -p 127.0.0.1:80:80 --name nginx-docker eussi/test-repo nginx -g "daemon off;"
docker run -d -p 127.0.0.1::80 --name nginx-docker eussi/test-repo nginx -g "daemon off;"
还可以使用以上命令将端口绑定到具体某个网卡的某个端口上,甚至可以使用/udp后缀来指定UDP端口。
Docker还提供了一个更简单的方式,即-P参数,该参数可以用来对外公开在Dockerfile中的EXPOSE指令中设置的所有端口,如:
docker run -d -P --name nginx-docker eussi/test-repo nginx -g "daemon off;"
该命令会将容器内的80端口对本地宿主机公开,并且绑定到宿主机的一个随机端口上。该命令会将用来构建该镜像的Dockerfile文件中EXPOSE指令指定的其他端口也一并公开。
下面,我们便可以通过宿主机绑定的端口访问容器中运行的nginx了,如:
$ curl localhost:32768
Hi, I am nginx in docker
6 Dockerfile指令
我们已经看过了一些 Dockerfile中可用的指令,如RUN和EXPOSE。但是,实际上还可以在Dockerfile中放入很多其他指令,这些指令包括CMD、 ENTRY POINT、ADD、COPY、 VOLUME、 WORKDIR、USER、 ONBUILD和ENV等。可以在http:/ /docs.docker.com/reference/builder/查看Dockerfile中可以使用的全部指令的清单。下面我们介绍一些常用的命令:
CMD
CMD指令用于指定一个容器启动时要运行的命令。这有点儿类似于RUN指令,只是RUN指令是指定镜像被构建时要运行的命令,而CMD是指定容器被启动时要运行的命令。这和使用 docker run命令启动容器时指定要运行的命令非常类似:
docker run -i -t centos /bin/bash -l
等效于:
CMD [ "/bin/bash" , "-l" ]
需要注意的是,要运行的命令是存放在一个数组结构中的。这将告诉 Docker按指定的原样来运行该命令。当然也可以不使用数组指定CMD指令,这时候 Docker会在指定的命令前加上/bin/sh-c。这在执行该命令的时候可能会导致意料之外的行为,所以Docker推荐一直使用以数组语法来设置要执行的命令。
最后,还需牢记,使用 docker run命令可以覆盖CMD指令。如果我们在 Dockerfile里指定了CMD指令,而同时在 docker run命令行中也指定了要运行的命令,命令行中指定的命令会覆盖 Dockerfile中的CMD指令。
在Dockerfile中只能指定一条CMD指令。如果指定了多条CMD指令,也只有最后一条CMD指令会被使用。如果想在启动容器时运行多个进程或者多条命令,可以考虑使用类似Supervisor这样的服务管理工具。
ENTRYPOINT
ENTRYPOINT指令与CMD指令非常类似,也很容易和CMD指令弄混。这两个指令到底有什么区别呢?为什么要同时保留这两条指令?正如我们己经了解到的那样,我们可以在docker run命令行中覆盖CMD指令。有时候,我们希望容器会按照我们想象的那样去工作这时候CMD就不太合适了。而 ENTRYPOINT指令提供的命令则不容易在启动容器时被覆盖。实际上,docker run命令行中指定的任何参数都会被当做参数再次传递给ENTRYPOINT指令中指定的命令。比如:
ENTRYPOINT [ "/usr/sbin/nginx" ]
类似于CMD指令,我们也可以在该指令中通过数组的方式为命令指定相应的参数,如
ENTRYPOINT [ "/usr/sbin/nginx", "-g", "daemon off; " ]
从上面看到的CMD指令可以看到,我们通过以数组的方式指定 ENTRYPOINT在想运行的命令前加入/bin/sh -c来避免各种问题。
我们也可以组合使用 ENTRYPOINT和CMD指令来完成一些巧妙的工作。比如添加以下内容:
ENTRYPOINT [ "/usr/sbin/nginx" ]
CMD [ "-h" ]
此时当我们启动一个容器时,任何在命令行中指定的参数都会被传递给Nginx守护进程。比如,我们可以指定-g" daemon off";参数让 Nginx守护进程以前台方式运行。如果在启动容器时不指定任何参数,则在CMD指令中指定的-h参数会被传递给Nginx守护进程,即Ngnix服务器会以/usr/sbin/nginx -h的方式启动,该命令用来显示Nginx的帮助信息。
这使我们可以构建一个镜像,该镜像既可以运行一个默认的命令,同时它也支持通过docker run命令行为该命令指定可覆盖的选项或者标志。
如果确实需要,也可以在运行时通过docker run的-- entrypoint标志覆盖ENTRYPOINT指令
WORKDIR
WNORKDIR指令用来在从镜像创建一个新容器时,在容器内部设置一个工作目录,ENTRYPOINT 和/或 CMD指定的程序会在这个目录下执行。
我们可以使用该指令为Dockerfile中后续的一系列指令设置工作目录,也可以为最终的容器设置工作目录。比如
WORKDIR /opt/webapp/db
run bundle install
WORKDIR /opt/webapp
ENTRYPOINT [ "rackup" ]
这里,我们将工作目录切换为/opt/webapp/db后运行了 bundle install命令,之后又将工作目录设置为/opt/webapp,最后设置了 ENTRYPOINT指令来启动rackup命令
可以通过-w标志在运行时覆盖工作目录,如
docker run -t -i -w /var/log centos pwd
该命令会将容器内的工作目录设置为/var/log。
ENV
ENV指令用来在镜像构建过程中设置环境变量,这个新的环境变量可以在后续的任何RUN指令中使用。
也能在其他指定中使用,如
ENV TARGET_DIR /opt/app WORKDIR $TARGET_DIR
在这里我们设定了一个新的环境变量TARGET_DIR,并在 WORKDIR中使用了它的值。因此实际上 WORKDIR指令的值会被设为/opt/app
这些环境变量也会被持久保存到从我们的镜像创建的任何容器中。如果我们在使用ENV TARGET_DIR /opt/app
指令构建的容器中运行env命令,将会看到该变量。
也可以使用 docker run命令行的-e标志来传递环境变量。这些变量将只会在运行时有效,如:
docker run -t -i -e "WEB_PORT=8080" centos env
USER
USER指令用来指定该镜像会以什么样的用户去运行,比如:
USER nginx
基于该镜像启动的容器会以nginx用户的身份来运行。我们可以指定用户名或UID以及组或GID,甚至是两者的组合。比如:
USER user
USER user:group
USER uid
USER uid:gid
USER user:gid
USER uid:group
也可以在docker run命令中通过-u选项来覆盖该指令指定的值,当然,指定用户需要存在。
如果不通过USER指令指定用户,默认用户为root。
VOLUME
VOLUME指令用来向基于镜像创建的容器添加卷。一个卷是可以存在于一个或者多个容器内的特定的目录,这个目录可以绕过联合文件系统,并提供如下共享数据或者对数据进行持久化的功能。
- 卷可以在容器间共享和重用。
- 一个容器可以不是必须和其他容器共享卷
- 对卷的修改是立时生效的。
- 对卷的修改不会对更新镜像产生影响。
- 卷会一直存在直到没有任何容器再使用它。
卷功能让我们可以将数据(如源代码)、数据库或者其他内容添加到镜像中而不是将这些内容提交到镜像中,并且允许我们在多个容器间共享这些内容。我们可以利用此功能来测试容器和内部的应用程序代码,管理日志,或者处理容器内部的数据库。
VOLUME [ "/opt/project " ]
这条指令将会为基于此镜像创建的任何容器创建一个名为/opt/project的挂载点。
我们也可以通过指定数组的方式指定多个卷,如:
VOLUME [ "/opt/project", "/data" ]
ADD
ADD指令用来将构建环境下的文件和目录复制到镜像中。比如,在安装一个应用程序时。ADD指令需要源文件位置和目的文件位置两个参数,如:
ADD software.lic /opt/application/software.lic
这里的ADD指令将会将构建目录下的software.lic文件复制到镜像中的/opt/application/software.lic。指向源文件的位置参数可以是一个URL,或者构建上下文或环境中文件名或者目录。不能对构建目录或者上下文之外的文件进行ADD操作。
在ADD文件时,Docker通过目的地址参数末尾的字符来判断文件源是目录还是文件。如果目标地址以/
结尾,那么 Docker就认为源位置指向的是一个目录。如果目的地址不是以/
结尾,那么Docker就认为源位置指向的是文件。
文件源也可以使用URL的格式,如:
Add http://wordpress.org/latest.zip /root/wordpress.zip
最后值得一提的是,ADD在处理本地归档文件( tar archive)时还有一些小魔法。如果将一个归档文件(合法的归档文件包括gzip、bzip2、xz)指定为源文件,Docker会自动将归档文件解开( unpack),如
ADD latest.tar.gz /var/www/wordpress/
这条命令会将归档文件解开到/var/www/ wordpress/目录下。Docker解开归档文件的行为和使用带-x选项的tar命令一样:该指令执行后的输出是原目的目录已经存在的内容加上归档文件中的内容。如果目的位置的目录下己经存在了和归档文
件同名的文件或者目录,那么目的位置中的文件或者目录不会被覆盖。
最后,如果目的位置不存在的话,Docker将会为我们创建这个全路径,包括路径中的任何目录。新创建的文件和目录的模式为0755,并且UID和GID都是0
ADD指令会使得构建缓存变得无效,这一点也非常重要。如果通过ADD指令向镜像添加个文件或者目录,那么这将使 Dockerfile中的后续指令都不能继续使用之前的构建缓存。
COPY
COPY指令非常类似于ADD,它们根本的不同是COPY只关心在构建上下文中复制本地文件,而不会去做文件提取( extraction)和解压( decompression)的工作。如:
COPY conf.d/ /etc/apache2/
文件源路径必须是一个与当前构建环境相对的文件或者目录,本地文件都放到和Dockerfile同一个目录下。不能复制该目录之外的任何文件,因为构建环境将会上传到Docker守护进程,而复制是在 Docker守护进程中进行的。任何位于构建环境之外的东西都是不可用的。COPY指令的目的位置则必须是容器内部的一个绝对路径。
任何由该指令创建的文件或者目录的UID和GID都会设置为0
如果源路径是一个目录,那么这个目录将整个被复制到容器中,包括文件系统元数据;如果源文件为任何类型的文件,则该文件会随同元数据一起被复制。在这个例子里,源路径以/
结尾,所以 Docker会认为它是目录,并将它复制到目的目录中,如果目的位置不存在, Docker将会自动创建所有需要的目录结构,就像mkdir -p
命令那样。
ONBUILD
ONBUILD指令能为镜像添加触发器( trigger)。当一个镜像被用做其他镜像的基础镜像时(比如你的镜像需要从某未准备好的位置添加源代码,或者你需要执行特定于构建镜像的环境的构建脚本),该镜像中的触发器将会被执行。
触发器会在构建过程中插入新指令,我们可以认为这些指令是紧跟在FROM之后指定的。触发器可以是任何构建指令,比如:
ONBUILD ADD. /app/src
ONBUILD RUN cd /app/src && make
上面的代码将会在创建的镜像中加入ONBUILD触发器,ONBUILD指令可以在镜像上运行 docker inspect命令来查看
ONBUILD触发器会按照父镜像中指定的顺序执行,并且只能被继承一次,不会在子孙镜像中执行。
这里有好几条指令是不能用在 ONBUILD指令中的,包括FROM、 MAINTAINER和 ONBUILD本身。之所以这么规定是为了防止在 Dockerfile构建过程中产生递归调用的问题。
6 将镜像推送到 Docker hub
镜像构建完毕之后,我们也可以将它上传到Docker hub上面去,这样其他人就能使用这个镜像了。比如,我们可以在组织内共享这个镜像,或者完全公开这个镜像。
Docker hub也提供了对私有仓库的支持,这是一个需要付费的功能,你可以将镜像存储到私有仓库中,这样只有你或者任何与你共享这个私有仓库的人才能访问该镜像。这样你就可以将机密信息或者代码放到私有镜像中,不必担心被公开访问了。
我们可以通过 docker push命令将镜像推送到 Docker hub,如:
$ docker push eussi/test-repo
The push refers to repository [docker.io/eussi/test-repo]
cbd8b1f9a714: Pushed
3fb7176a7dc6: Pushed
db9f2b70b0c9: Pushed
d69483a6face: Layer already exists
latest: digest: sha256:631c44c8297ffa17c2d3e2f6ce12dd4df7daec98d18dab91bae44779c559ae79 size: 1159
efefa7579bc6: Pushed
d69483a6face: Layer already exists
test: digest: sha256:d8817e3b1afcfdad87936edda56314052b1c1b8597a3ee893cd18660bb92ef28 size: 736
我们使用了一个名为eussi/test-repo的用户仓库,成功地将镜像推送到了Docker hub。我们将会使用自己的用户ID,这个ID也是我们前面创建的,并选择了一个合法的镜像名(如 youruser/yourlmage)。
上传成功之后,我们可以在 Docker Hub看到我们上传的镜像。
自动构建
除了从命令行构建和推送镜像,Docker Hub还允许我们定义自动构建( Automated Builds)。为了使用自动构建,我们只需要将 GitHub或 BitBucket中含有 Dockerfile文件的仓库连接到 Docker hub即可。向这个代码仓库推送代码时,将会触发一次镜像构建活动并创建一个新镜像。在之前该工作机制也被称为可信构建( Trusted Build)。
注意自动构建同样支持私有 GitHub和 BitBucket仓库。
具体操作只是一些界面的简单操作而已,这里不再详述,如需要,可自行网上搜索。
7 删除镜像
docker rmi 镜像名 [镜像名 ...]
如果删除Docker hub上的镜像需要登录Docker hub通过界面删除。
还可以使用以下命令删除,如:
docker rmi $(docker images -a -q)
8 运行自己的Registry
从docker容器中运行一个Registry很简单,如:
$ docker run -p 5000:5000 registry
Unable to find image 'registry:latest' locally
latest: Pulling from library/registry
c87736221ed0: Pull complete
1cc8e0bb44df: Pull complete
54d33bcb37f5: Pull complete
e8afc091c171: Pull complete
b4541f6d3db6: Pull complete
Digest: sha256:3b00e5438ebd8835bcfa7bf5246445a6b57b9a50473e89c02ecc8e575be3ebb5
Status: Downloaded newer image for registry:latest
time="2019-04-05T06:13:41.65385353Z" level=warning msg="No HTTP secret provided - generated random secret. This may cause problems with uploads if multiple registries are behind a load-balancer. To provide a shared secret, fill in http.secret in the configuration file or set the REGISTRY_HTTP_SECRET environment variable." go.version=go1.11.2 instance.id=ec088d53-9b67-4da7-a6ad-f8319999e6a9 service=registry version=v2.7.1
time="2019-04-05T06:13:41.653924388Z" level=info msg="redis not configured" go.version=go1.11.2 instance.id=ec088d53-9b67-4da7-a6ad-f8319999e6a9 service=registry version=v2.7.1
time="2019-04-05T06:13:41.654038206Z" level=info msg="Starting upload purge in 28m0s" go.version=go1.11.2 instance.id=ec088d53-9b67-4da7-a6ad-f8319999e6a9 service=registry version=v2.7.1
time="2019-04-05T06:13:41.663172169Z" level=info msg="using inmemory blob descriptor cache" go.version=go1.11.2 instance.id=ec088d53-9b67-4da7-a6ad-f8319999e6a9 service=registry version=v2.7.1
time="2019-04-05T06:13:41.664410214Z" level=info msg="listening on [::]:5000" go.version=go1.11.2 instance.id=ec088d53-9b67-4da7-a6ad-f8319999e6a9 service=registry version=v2.7.1
该命令将启动一个运行Registry应用的容器。
那么我们如何使用Registory呢?如下命令:
$ docker tag 98ab6a8ede3a localhost:5000/eussi/test-repo
$ docker push localhost:5000/eussi/test-repo
The push refers to repository [localhost:5000/eussi/test-repo]
cbd8b1f9a714: Pushed
3fb7176a7dc6: Pushed
db9f2b70b0c9: Pushed
d69483a6face: Pushed
latest: digest: sha256:631c44c8297ffa17c2d3e2f6ce12dd4df7daec98d18dab91bae44779c559ae79 size: 1159
$ docker run -i -t localhost:5000/eussi/test-repo /bin/bash
[root@dec738af55ae /]# whereis nginx
nginx: /usr/sbin/nginx /usr/lib64/nginx /etc/nginx /usr/share/nginx
这是在防火墙后面部署自己的 Docker Registry的最简单的方式。并没有解释如何配置或者管理 Registry.。如果想深入了解如何配置认证和管理后端镜像存储方式,以及如何管理Registry等详细信息,可以在Docker Registry文档查看完整的配置和部署说明
9 其他可选 Registry服务
也有很多其他公司和服务提供定制的 Docker Registry服务。
如,Quay
Quay服务提供了私有的 Registry托管服务,允许你上传公共的或者私有的容器。目前它提供了免费的无限制的公共仓库托管服务,如果想托管私有仓库,它还提供了一系列的可伸缩计划。