0x00 概述

在掌握 Dockerfile 的基本使用方法后,我们再来了解一些在开发中使用 Dockerfile 的技巧。这一小节的展现方式与之前的略有不同,其主要来自阅读收集和我自身在使用中的最佳实践。也许这里面介绍的不是最为标准或是合乎规范的方式,但一定是能够直接帮助大家在开发中使用 Docker 提升生产力的方式。下面就让我们来看看这些关于 Dockerfile 的使用技巧吧。

0x01 构建中使用变量

在实际编写 Dockerfile 时,与搭建环境相关的指令会是其中占有大部分比例的指令。在搭建程序所需运行环境时,难免涉及到一些可变量,例如依赖软件的版本,编译的参数等等。我们可以直接将这些数据写入到 Dockerfile 中完全没有问题,有问题的是这些可变量我们会经常调整,在调整时就需要我们到 Dockerfile 中找到它们并进行更改,如果只是简单的 Dockerfile 文件尚且好说,但如果是相对复杂或是存在多处变量的 Dockerfile 文件,这个工作就变得繁琐而让人烦躁了。

在 Dockerfile 里,我们可以用 ARG 指令来建立一个参数变量,我们可以在构建时通过构建指令传入这个参数变量,并且在 Dockerfile 里使用它。

例如,我们希望通过参数变量控制 Dockerfile 中某个程序的版本,在构建时安装我们指定版本的软件,我们可以通过 ARG 定义的参数作为占位符,替换版本定义的部分。

FROM debian:stretch-slim

## ......

ARG TOMCAT_MAJOR
ARG TOMCAT_VERSION ## ...... RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz" ## ......

在这个例子里,我们将 Tomcat 的版本号通过 ARG 指令定义为参数变量,在调用下载 Tomcat 包时,使用变量替换掉下载地址中的版本号。通过这样的定义,就可以让我们在不对 Dockerfile 进行大幅修改的前提下,轻松实现对 Tomcat 版本的切换并重新构建镜像了。

如果我们需要通过这个 Dockerfile 文件构建 Tomcat 镜像,我们可以在构建时通过 docker build 的 --build-arg 选项来设置参数变量。

$ sudo docker build --build-arg TOMCAT_MAJOR= --build-arg TOMCAT_VERSION=8.0. -t tomcat:8.0 ./tomcat

0x02 环境变量

环境变量也是用来定义参数的东西,与 ARG 指令相类似,环境变量的定义是通过 ENV 这个指令来完成的。

FROM debian:stretch-slim

## ......

ENV TOMCAT_MAJOR
ENV TOMCAT_VERSION 8.0. ## ...... RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"

环境变量的使用方法与参数变量一样,也都是能够直接替换指令参数中的内容。

与参数变量只能影响构建过程不同,环境变量不仅能够影响构建,还能够影响基于此镜像创建的容器。环境变量设置的实质,其实就是定义操作系统环境变量,所以在运行的容器里,一样拥有这些变量,而容器中运行的程序也能够得到这些变量的值。

另一个不同点是,环境变量的值不是在构建指令中传入的,而是在 Dockerfile 中编写的,所以如果我们要修改环境变量的值,我们需要到 Dockerfile 修改。不过即使这样,只要我们将 ENV 定义放在 Dockerfile 前部容易查找的地方,其依然可以很快的帮助我们切换镜像环境中的一些内容。

由于环境变量在容器运行时依然有效,所以运行容器时我们还可以对其进行覆盖,在创建容器时使用 -e 或是 --env 选项,可以对环境变量的值进行修改或定义新的环境变量。

$ sudo docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7

事实上,这种用法在我们开发中是非常常见的。也正是因为这种允许运行时配置的方法存在,环境变量和定义它的 ENV 指令,是我们更常使用的指令,我们会优先选择它们来实现对变量的操作。

关于环境变量是如何能够帮助我们更轻松的处理 Docker 镜像和容器使用等问题,我们会在下一节中进行实际展示,通过例子大家能够更容易理解它的原理。

另外需要说明一点,通过 ENV 指令和 ARG 指令所定义的参数,在使用时都是采用 $ + NAME 这种形式来占位的,所以它们之间的定义就存在冲突的可能性。对于这种场景,大家只需要记住,ENV 指令所定义的变量,永远会覆盖 ARG 所定义的变量,即使它们定时的顺序是相反的。

0x03 合并命令

在上一节我们展示的完整的官方 Redis 镜像的 Dockerfile 中,我们会发现 RUN 等指令里会聚合下大量的代码。

事实上,下面两种写法对于搭建的环境来说是没有太大区别的。

RUN apt-get update; \
apt-get install -y --no-install-recommends $fetchDeps; \
rm -rf /var/lib/apt/lists/*;
RUN apt-get update
RUN apt-get install -y --no-install-recommends $fetchDeps
RUN rm -rf /var/lib/apt/lists/*

那为什么我们更多见的是第一种形式而非第二种呢?这就要从镜像构建的过程说起了。

看似连续的镜像构建过程,其实是由多个小段组成。每当一条能够形成对文件系统改动的指令在被执行前,Docker 先会基于上条命令的结果启动一个容器,在容器中运行这条指令的内容,之后将结果打包成一个镜像层,如此反复,最终形成镜像。

所以说,我们之前谈到镜像是由多个镜像层叠加而得,而这些镜像层其实就是在我们 Dockerfile 中每条指令所生成的。

了解了这个原理,大家就很容易理解为什么绝大多数镜像会将命令合并到一条指令中,因为这种做法不但减少了镜像层的数量,也减少了镜像构建过程中反复创建容器的次数,提高了镜像构建的速度。

0x04 构建缓存

Docker 在镜像构建的过程中,还支持一种缓存策略来提高镜像的构建速度。

由于镜像是多个指令所创建的镜像层组合而得,那么如果我们判断新编译的镜像层与已经存在的镜像层未发生变化,那么我们完全可以直接利用之前构建的结果,而不需要再执行这条构建指令,这就是镜像构建缓存的原理。

那么 Docker 是如何判断镜像层与之前的镜像间不存在变化的呢?这主要参考两个维度,第一是所基于的镜像层是否一样,第二是用于生成镜像层的指令的内容是否一样。

基于这个原则,我们在条件允许的前提下,更建议将不容易发生变化的搭建过程放到 Dockerfile 的前部,充分利用构建缓存提高镜像构建的速度。另外,指令的合并也不宜过度,而是将易变和不易变的过程拆分,分别放到不同的指令里。

在另外一些时候,我们可能不希望 Docker 在构建镜像时使用构建缓存,这时我们可以通过 --no-cache 选项来禁用它。

$ sudo docker build --no-cache ./webapp

0x05 搭配 ENTRYPOINT 和 CMD

上一节我们谈到了 ENTRYPOINT 和 CMD 这两个命令,也解释了这两个命令的目的,即都是用来指定基于此镜像所创建容器里主进程的启动命令的。

两个指令的区别在于,ENTRYPOINT 指令的优先级高于 CMD 指令。当 ENTRYPOINT 和 CMD 同时在镜像中被指定时,CMD 里的内容会作为 ENTRYPOINT 的参数,两者拼接之后,才是最终执行的命令。

为了更好的让大家理解,这里索性列出所有的 ENTRYPOINT 与 CMD 的组合,供大家参考。

有的读者会存在疑问,既然两者都是用来定义容器启动命令的,为什么还要分成两个,合并为一个指令岂不是更方便吗?

这其实在于 ENTRYPOINT 和 CMD 设计的目的是不同的。ENTRYPOINT 指令主要用于对容器进行一些初始化,而 CMD 指令则用于真正定义容器中主程序的启动命令。

另外,我们之前谈到创建容器时可以改写容器主程序的启动命令,而这个覆盖只会覆盖 CMD 中定义的内容,而不会影响 ENTRYPOINT 中的内容。

我们依然以之前的 Redis 镜像为例,这是 Redis 镜像中对 ENTRYPOINT 和 CMD 的定义。

## ......

COPY docker-entrypoint.sh /usr/local/bin/

ENTRYPOINT ["docker-entrypoint.sh"]

## ......

CMD ["redis-server"]

可以很清晰的看到,CMD 指令定义的正是启动 Redis 的服务程序,而 ENTRYPOINT 使用的是一个外部引入的脚本文件。

事实上,使用脚本文件来作为 ENTRYPOINT 的内容是常见的做法,因为对容器运行初始化的命令相对较多,全部直接放置在 ENTRYPOINT 后会特别复杂。

我们来看看 Redis 中的 ENTRYPOINT 脚本,可以看到其中会根据脚本参数进行一些处理,而脚本的参数,其实就是 CMD 中定义的内容。

#!/bin/sh
set -e # first arg is `-f` or `--some-option`
# or first arg is `something.conf`
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
set -- redis-server "$@"
fi # allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '' ]; then
find . \! -user redis -exec chown redis '{}' +
exec gosu redis "$0" "$@"
fi exec "$@"

这里我们要关注脚本最后的一条命令,也就是 exec "$@"。在很多镜像的 ENTRYPOINT 脚本里,我们都会看到这条命令,其作用其实很简单,就是运行一个程序,而运行命令就是 ENTRYPOINT 脚本的参数。反过来,由于 ENTRYPOINT 脚本的参数就是 CMD 指令中的内容,所以实际执行的就是 CMD 里的命令。

所以说,虽然 Docker 对容器启动命令的结合机制为 CMD 作为 ENTRYPOINT 的参数,合并后执行 ENTRYPOINT 中的定义,但实际在我们使用中,我们还会在 ENTRYPOINT 的脚本里代理到 CMD 命令上。

相对来说,Redis 的 ENTRYPOINT 内容还是简单的,在掌握了 ENTRYPOINT 的相关作用后,大家可以尝试阅读和编写一些复杂的 ENTRYPOINT 脚本。

0x06 临摹案例

上面提及的几项只是几个比较常见的 Dockerfile 最佳实践,其实在编写 Dockerfile 时,还有很多不成文的小技巧。

想要学好 Dockerfile 的编写,阅读和思考前人的作品是必不可少的。

前面我们介绍了,Docker 官方提供的 Docker Hub 是 Docker 镜像的中央仓库,它除了镜像丰富之外,给我们带来的另一项好处就是其大部分镜像都是能够直接提供 Dockerfile 文件给我们参考的。

要得到镜像的 Dockerfile 文件,我们可以进入到镜像的详情页面,在介绍中,镜像作者们通常会直接把 Dockerfile 的连接放在那里。

除此之外,进入到 Dockerfile 这个栏目下,我们也能够直接看到镜像 Dockerfile 的内容。在页面的右侧,还有进入 Dockerfile 源文件的连接,如果在 Dockerfile 中有引入其他的文件,我们可以通过这个连接访问到。

另外,我自己也制作了一些软件的镜像,大家可以访问 GitHub 上的项目地址,查阅其中的 Dockerfile 内容:github.com/cogset 。

Docker学习笔记之常见 Dockerfile 使用技巧的更多相关文章

  1. Docker学习笔记之通过 Dockerfile 创建镜像

    0x00 概述 由于 Docker 镜像的结构优势,使它的占用空间远小于普通的虚拟机镜像,而这就大幅减少了 Docker 镜像在网络或者其他介质中转移所花费的时间,进而提高了我们进行迁移部署的效率.不 ...

  2. docker学习笔记18:Dockerfile 指令 VOLUME 介绍

    在介绍VOLUME指令之前,我们来看下如下场景需求: 1)容器是基于镜像创建的,最后的容器文件系统包括镜像的只读层+可写层,容器中的进程操作的数据持久化都是保存在容器的可写层上.一旦容器删除后,这些数 ...

  3. docker学习笔记17:Dockerfile 指令 ONBUILD介绍

    ONBUILD指令可以为镜像添加触发器.其参数是任意一个Dockerfile 指令. 当我们在一个Dockerfile文件中加上ONBUILD指令,该指令对利用该Dockerfile构建镜像(比如为A ...

  4. docker学习笔记14:Dockerfile 指令 ENV介绍

    ENV指令用来在镜像构建过程中设置环境变量.我们来看一个Dockerfile的例子: #test FROM ubuntu MAINTAINER hello ENV MYDIR /mydir RUN m ...

  5. docker学习笔记13:Dockerfile 指令 WORKDIR介绍

    Dockerfile中的WORKDIR指令用于指定容器的一个目录, 容器启动时执行的命令会在该目录下执行. 相当于设置容器的工作目录了.我们来看一个dockerfile文件 #test FROM ub ...

  6. docker学习笔记12:Dockerfile 指令 ENTRYPOINT介绍

    本文介绍Dockerfile的 ENTRYPOINT指令的含义. 先回顾下CMD指令的含义,CMD指令可以指定容器启动时要执行的命令,但它可以被docker run命令的参数覆盖掉. ENTRYPOI ...

  7. docker学习笔记11:Dockerfile 指令 CMD介绍

    我们知道,通过docker run 创建并启动一个容器时,命令的最后可以指定容器启动后在容器内立即要执行的指令,如: docker run -i -t ubunu /bin/bash   //表示容器 ...

  8. Docker学习笔记_使用Dockerfile创建flask的一个镜像

    一.实验环境 1.宿主机OS:Win10 64位 2 .虚拟机OS:Ubuntu18.04 64位    虚拟机名称:Ubuntu18VM1   虚拟机IP:192.168.8.25 3.账号:doc ...

  9. docker学习笔记-05:DockerFile解析

    一.DockerFile是什么 1.DockerFile是用来构建docker镜像的构建文件,是由一系列参数和命令构成的脚本. 2.构建三步骤: 手动编写一个dockerfile文件,然后直接dock ...

随机推荐

  1. Centos ssh 限制ip访问

    要确定客户端计算机是否允许连接到服务,TCP包装器将引用以下两个文件,这两个文件通常称为主机访问文件: /etc/hosts.allow /etc/hosts.deny 当TCP包裹服务接收到客户端请 ...

  2. 第一次java测试有感

    今天下午的Java测试体会深刻,真的可能我一暑假学的还没有今天一下午学的多.但通过今天一下午地与Java近距离接触 ,我感受到我与真正的Java距离还是特别远的.以后我的路还很长,我对Java仍然还是 ...

  3. 解放你生产力的10款效率工具,赶紧mark起来

    聪明的人总是善用工具 社会的快度发展赋予我们的一个幸运之处在于:你可以通过读书或使用工具去改善自身的处境甚至是阶层. 在父母那个时代,通过熟练使用办公软件,可以使一个在水泥地做帮工的青年走进格子间.而 ...

  4. IdentityServer4:Endpoint

    Endpoint的概念在IdentityServer里其实就是一些资源操作的url地址:如同Restful API里面的Endpoint是概念: 那么可以通过你自己的授权服务端得到相对应的地址与信息: ...

  5. cocosStudio制作ScrollView并在cocos2dx 3.0中使用。

    使用cocosStudio制作界面基本已成为基础了,之前都是拖动一些 Image.Button的小控件,再用到层容器和滚动层的时候,习惯性的用拖动来改变控件的大小.但是你在把其他的控件拖动到上面的时候 ...

  6. Flume:sink.type=hive

    Flume以Kafka为Source,以Hive为Sink进行数据转存. 业务背景:公司要求将某四川城市的卡口数据实时接入大数据平台中,历史数据可以通过Hive进行Load,也就是增量数据的对接问题. ...

  7. HDU 2254 奥运(矩阵+二分等比求和)

    奥运 [题目链接]奥运 [题目类型]矩阵+二分等比求和 &题解: 首先离散化城市,之后就是矩阵快速幂了,但让求的是A^(t1)+A^(t1+1)+...+A^(t2),我先想的是打表,但时间真 ...

  8. hibernate中configuration和配置文件笔记

    hibernate的核心类和接口 Configuration类 作用:(1)读取hibernate.cfg.xml文件 (2)管理对象关系映射文件<mapping resource=" ...

  9. tp视图模板

    <?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { ...

  10. Unity shader学习之屏幕后期处理效果之均值模糊

    均值模糊,也使用卷积来实现,之不过卷积中每个值均相等,且相加等于1. 代码如下, 子类: using UnityEngine; public class MeanBlurRenderer : Post ...