使用 Dockerfile 构建 Docker 镜像

构建基础镜像

想要构建一个系统基础镜像,可以借助 debootstrap 工具:

[root@server4 docker]$ yum install -y debootstrap

下载所需文件:

[root@server4 docker]$ debootstrap --arch amd64 trusty ubutu-trusty http://mirrors.163.com/ubuntu/

提交生成基础镜像,名为 ubase:0.1

[root@server4 docker]$ cd ubutu-trusty/
[root@server4 ubutu-trusty]$ tar -c .|docker import - ubase:0.1
[root@server4 ubutu-trusty]$ docker images
REPOSITORY                     TAG       IMAGE ID       CREATED         SIZE
ubase                          0.1       75d5ee4917e0   8 seconds ago   228MB
[root@server4 ubutu-trusty]$ docker run -it ubase:0.1 /bin/bash
root@e85c74d94582:/# cat /etc/os-release     
NAME="Ubuntu"
VERSION="14.04, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"

Dockerfile

Dockerfile 是由 Docker 提供的进行镜像自动化构建的配置文件,包含所有用于构建镜像所执行的命令。

Dockerfile 本质上是一个简单的文本文件,使用 DSL (Domain Specific Language) 语法,其内容包括三种形式:

  • 注释行:使用 # 开头的文本行,用于记录一些额外的说明文字。例如:# Echo message
  • 指令行:指令行分两部分,行首是指令名称 (INSTRUCTION),一般采用全部大写字母格式。后面是参数 (arguments)。例如:RUN echo 'Building'
  • 解析指令行:解析指令行 (Parser Directives) 也是使用 # 作为开头,主要作用是提供一些解析 Dockerfile 需要使用的参数,一般很少用到。例如:# directive=value

通过 Dockerfile 建立镜像的每一步操作(执行类似 docker commit 操作)都会生成一层镜像,在建立镜像层时本地已存在的镜像层会直接采用。无论什么时候,只要某层发生变化,其上面所有层的缓存都会失效。

当使用 Dockerfile 构建失败时,可以使用 docker run 命令运行基于构建已经成功的最后一步创建的镜像来启动容器,调试失败的构建命令。之后修改 Dockerfile 文件再次尝试构建。

为了减少镜像层数量,可以通过 && 合并一些构建过程中的脚本命令。

基础指令

基础指令是控制 Dockerfile 整体性质的指令。

FROM

FROM 指令用来指定要构建的镜像是基于哪个镜像建立。作为必备指令,FROM 指令必须作为 Dockerfile 的第一条指令。允许出现多个 FROM 指令,以每个 FROM 指令为界限生成不同的镜像。

FROM 指令的格式为:FROM <image>:<tag>FROM <image>@<digest>,其中 <tag><digest> 都是可选项。

例如以 mysql:5.6 镜像作为基础镜像:FROM mysql:5.6

如果不以任何镜像为基础,那么写法为:FROM scratch

为了保证镜像精简,可以选择体积较小的镜像如 AlpineDebian 作为基础镜像。

MAINTAINER

MAINTAINER 指令用于提供镜像的作者信息,不是必须提供的。

其格式为:MAINTAINER <name>

例如填写制作者的联系方式:MAINTAINER bbq@123.com

控制指令

控制指令是 Dockerfile 的核心部分,通过控制指令来描述整个镜像的构建过程。

RUN

RUN 指令用于指定构建镜像时需要执行的操作。每条 RUN 指令执行后,在当前镜像层创建一个新的镜像层。

  • 指令格式:RUN command param1 param2 ...

    此种形式下的命令是以 shell 来执行操作,在 Linux 上默认选择 /bin/sh 作为 shell 程序。当指令较长时,可以使用 \ 来拆分成多行。例如,执行安装多个程序的命令:

    RUN yum install -y \
            gcc \
            make \
            curl
  • 指令格式:RUN ["executable","param1","param2", ...]

    此种形式下的命令是将命令和参数都使用双引号进行引用,并逐个传递。使用这种方式可以避免基础镜像中没有 shell 程序或临时切换 shell 程序的问题。例如,使用 /bin/bash 作为 shell:

    RUN ["/bin/bash", "-c", "echo build"]

在使用 RUN 指令时,Docker 判断是否采用缓存构建的依据是给出的指令是否与生成缓存所使用的指令一致。如果执行结果有差异,也会采用缓存中的数据。如果希望忽略缓存,可以在执行 docker build 命令时加入 --no-cache 选项。

在构建时安装程序后,需要及时清理缓存和临时文件,以减少镜像体积。

WORKDIR

WORKDIR 指令用于在构建过程中切换工作目录,可以多次指定。可以使用绝对路径或相对路径来指定目录。

指令格式:WORKDIR /root/bin

也可以在 WORKDIR 指令中使用环境变量。例如,调用 BASEDIR 变量:

ENV BASEDIR /project
WORKDIR $BASEDIR/html

如果目录不存在,会自动创建。

ONBUILD

ONBUILD 可以为镜像添加触发器(trigger),后面指定的指令不会在当前镜像构建时执行,而是在其他镜像通过 FROM 调用时执行。一般对于使用 ONBUILD 指令的镜像,在镜像名后会加上 -onbuild

指令格式:ONBUILD INSTRUCTION arguments

ONBUILD 指令只会在构建子镜像中执行。子镜像构建完成后,指令会消失而不会继承到后续的镜像中。

例如:ONBUILD ADD . /app/src

引入指令

主要作用是将文件加入到构建镜像中。

ADD

可以使用 ADD 指令将文件从外部传递到镜像内部。

指令格式:ADD <src>... <dest>ADD ["<src>",... "<dest>"]。后一种形式用于处理带空格的文件名。

指令用法如下:

  • 指定源目标时,需要使用相对路径。也就是以 Dockerfile 文件所在路径为基准目录,源目标不能超出基准目录。
  • 如果指定的源目标是压缩文件(gzip、bzip2、xz),Docker 会自动解压文件内容到目标目录。
  • 如果指定的源目标是目录,目录本身不会被复制到镜像中,只会复制目录中的内容。
  • 可以指定 URL 地址作为源目标。
  • 可以使用多个源目标和通配符,此时目标目录必须以 / 结尾。
  • 如果指定的源目标与构建缓存中的文件不一致,Docker 会忽略缓存。
  • 可以使用绝对路径或相对路径来指定目标目录,相对路径是以使用 WORKDIR 指令设置的工作目录为基准。
  • 如果目标目录不存在,会自动创建。

例如,将当前目录下的所有 txt 格式文件复制到镜像的 /work/ 目录下:ADD *.txt /work/

COPY

COPY 指令与 ADD 指令的用法相同,主要区别是 COPY 指令不能指定 URL 地址,也不会自动解压文件。

执行指令

执行指令能够指定通过镜像建立容器时,容器默认执行的命令。Dockerfile 中至少有一条 CMD 或 ENTRYPOINT 指令。

CMD

CMD 指令用来指定由镜像创建的容器中的主体程序,也就是配置镜像的默认入口程序。有两种调用格式:

  • 指令格式:CMD command param1 param2 ...CMD ["executable","param1","param2", ...]

    用法和 RUN 指令类似,都是取决于是否使用 Shell 程序来执行命令。前一种示例:CMD echo "test." | wc -

    一般用后一种格式绑定执行程序,例如:CMD ["/usr/bin/wc","--help"]

  • 指令格式:CMD ["param1","param2", ...]

    将给出的参数传给 ENTRYPOINT 指令给出的程序。

因为容器只会绑定一个应用程序,所以 Dockerfile 中只存在一个 CMD 指令。设置多个 CMD 指令则以最后一个为准。

此外,CMD 指令可以被创建容器时自定义的启动指令给覆盖掉。

ENTRYPOINT

ENTRYPOINT 指令用于设置主程序启动前的准备工作。例如要在容器中额外启动 sshd 服务,可以把这些服务写到脚本中,通过 ENTRYPOINT 指令来启动。在启动脚本中通过 exec 命令启动的服务,可让服务在容器中使用 PID 1 作为进程号。

Dockerfile 中同样只能有一个 ENTRYPOINT。可以被创建容器时加入的 --entrypoint 参数给覆盖掉。

指令格式:ENTRYPOINT command params ...ENTRYPOINT ["executable","param1","param2", ...]。两种指令格式在效果上同 CMD 一样。使用 Shell 方式运行时,入口程序不能接收 SIGTERM。

如果同时存在 CMD 和 ENTRYPOINT 指令指定可执行命令,CMD 会在 ENTRYPOINT 之前运行。当 CMD 指令不可执行时,所有 CMD 指令或通过 docker run 方式指定的命令作为参数拼接到 ENTRYPOINT 指令给出的命令之后,传给 ENTRYPOINT 指令给出的程序。

当需要把容器当作一个命令行工具使用时,可以通过 ENTRYPOINT 设置镜像的入口程序。例如配合 CMD 命令启动时显示帮助文档:

ENTRYPOINT ["Pygame"]
CMD ["--help"]

配置指令

若要对容器进行相关环境或网络等配置,可以通过配置指令来实现。

EXPOSE

如果容器中的应用程序需要让其他客户端访问到它提供的端口,需要通过 EXPOSE 指令显式给出对外提供的端口号。

指令格式:EXPOSE <port>

只需要将开放端口逐一传入即可,多个端口用空格隔开。例如开放容器的 22、80 和 443 端口:EXPOSE 22 80 443

还可以设置开放的端口协议:EXPOSE 11211/tcp 11211/udp

创建容器时使用 -P 参数能将 EXPOSE 中端口映射到主机上随机端口。使用 -p 可以映射 EXPOSE 中没有列出的端口。

ENV

使用 ENV 指令来设置环境变量,有两种设置格式:

  • 指令格式:ENV <key> <value>

    在键名 key 之后的数据都会被视为环境变量的值。例如设置 myName 值为 John Doe:ENV myName John Doe

  • 指令格式:ENV <key>=<value>

    可以一次指定多个环境变量,并且可以使用 \ 进行换行连接。由于每使用一次 ENV 指令都会生成镜像层,因此建议使用此格式来定义环境变量。例如:ENV myCat=fluffy

环境变量能够被继承,基础镜像中的环境变量会继承到构建镜像中。此外,环境变量会存在基于构建镜像运行的容器中。

容器创建时,可以通过 --env 参数来新增或修改环境变量。

LABEL

LABEL 指令用来设置镜像的标签。

指令格式:LABEL <key>=<value> <key>=<value> ...

每个 LABEL 指令都会产生一个新的镜像层,所以尽量将标签记录在一个 LABEL 指令中。

例如:LABEL version="1.0" description="Web Server"

VOLUME

创建一个数据挂载点用于持久化。

指令格式:VOLUME ["/DIR"]VOLUME /SRC /DST

运行容器时可以从本地或其他容器挂载数据卷。例如把 /data 挂载到容器中的 /etc/dfsVOLUME /data /etc/dfs

如果容器中的目标目录不存在,会自动新建。

HEALTHCHECK

配置容器的健康检查。

指令格式:HEALTHCHECK [OPTIONS] CMD command

根据所执行命令返回值是否为 0 来判断。若返回值为 1,则容器无法正常工作。

OPTIONS 可以指定以下参数:

  • –interval=DURATION(默认值:30s):检查的时间间隔。
  • –timeout=DURATION(默认值:30s):每次检查等待结果的超时时间。
  • –retries=N(默认值:3):重试尝试次数。

例如:HEALTHCHECK --interval=5m --timeout=3s CMD curl -f http://localhost/ || exit 1

USER

USER 指令用于设置执行用户或 UID。如果容器中的应用程序在运行时不需要特权,可以通过 USER 指令将应用程序的所有者设为非 root 用户。此时,在容器中新建用户和组需要指定 UID 和 GID,因为每次构建镜像时系统会分配不同的 UID/GID。

指令格式:USER root

USER 指令对其后的 RUN、CMD 和 ENTRYPOINT 指令都会起作用。

ARG

和 ENV 的作用类似,ARG 用于定义只在镜像构建过程中使用的局部变量。

指令格式:ARG <name>=<default>

ARG 从定义它的地方开始生效,而不是调用的地方。例如:ARG build_user=www

也可以仅声明变量名而不指定变量值,通过外部传递变量(使用 docker build --build-arg 来赋值)。例如:ARG site

如果 ENV 指令和 ARG 指令定义了相同的变量,以 ENV 定义的环境变量为准。

Docker 内置了一些镜像创建变量:HTTP_PROXY、HTTPS_PROXY、FTP_PROXY、NO_PROXY。

STOPSIGNAL

用来定义程序停止的信号,例如:STOPSIGNAL 9

SHELL

使用 SHELL 指令可以设定 CMD 和 ENTRYPOINT 等指令默认使用的 shell 程序。

指令格式:SHELL ["executable", "parameters"]

例如,修改默认执行 shell 为 /bin/bashSHELL ["/bin/bash", "-c"]

特殊用法

在 Dockerfile 中除了指令外的一些特殊的使用方法。

环境变量

通过 ENV 定义的环境变量,可以在之后的命令中调用,调用方式有以下几种:

  • $变量名:这是最普通的调用方法,例如:RUN echo $HOME
  • ${变量名}:用花括号将变量名括起来,可避免出现歧义。例如:RUN echo ${HOME}
  • ${变量:-替换内容}:当变量不存在时,用替换内容代替变量。
  • ${变量:+替换内容}:当变量已定义和复制时,替换内容会替换占位符。变量不存在时,占位符会被直接清除。

指令解析

针对不同系统使用的特殊符号不同,而引用解析指令行功能来消除歧义。

例如在 Linux 中使用 \ 符号来进行命令换行,而在 Windows 系统中作为目录分隔符使用。可以设置 escape 的值来将换行分隔符设置为 @,这样 Windows 路径中的 \ 就不会被错误解析了:

# escape=@

FROM windowsservercore
COPY test.txt c:\
RUN dir c:\

忽略文件

可以使用类似 Git 忽略功能的文件 .dockerignore,来对构建镜像时的一些敏感信息或无用文件进行忽略。

在 Docker 中,通常倾向于忽略掉所有文件,只保留确定需要传入镜像的文件。

例如保留 conf.xml 文件和 config/user.xml 文件:

# Keep files
*
!conf.xml
!config/user.xml

示例样本

以下是一些 Dockerfile 示例样本。

普通样本

带说明内容的构建样本:

# This my first nginx Dockerfile
# Version 1.0
# Base images 基础镜像
FROM centos
#MAINTAINER 维护者信息
MAINTAINER tianfeiyu 
#ENV 设置环境变量
ENV PATH /usr/local/nginx/sbin:$PATH
#ADD  文件放在当前目录下,拷过去会自动解压
ADD nginx-1.8.0.tar.gz /usr/local/  
ADD epel-release-latest-7.noarch.rpm /usr/local/  
#RUN 执行以下命令 
RUN rpm -ivh /usr/local/epel-release-latest-7.noarch.rpm
RUN yum install -y wget lftp gcc gcc-c++ make openssl-devel pcre-devel pcre && yum clean all
RUN useradd -s /sbin/nologin -M www
#WORKDIR 相当于cd
WORKDIR /usr/local/nginx-1.8.0 
RUN ./configure --prefix=/usr/local/nginx --user=www --group=www --with-http_ssl_module --with-pcre && make && make install
RUN echo "daemon off;" >> /etc/nginx.conf
#EXPOSE 映射端口
EXPOSE 80
#CMD 运行以下命令
CMD ["nginx"]

说明样本

另外一个 Nginx 镜像构建样本:

## Set the base image to CentOS  基于centos镜像
FROM centos
# File Author / Maintainer  作者信息
MAINTAINER test test@example.com
# Install necessary tools  安装一些依赖的包
RUN yum install -y pcre-devel wget net-tools gcc zlib zlib-devel make openssl-devel
# Install Nginx  安装nginx
ADD http://nginx.org/download/nginx-1.8.0.tar.gz .  # 添加nginx的压缩包到当前目录下
RUN tar zxvf nginx-1.8.0.tar.gz  # 解包
RUN mkdir -p /usr/local/nginx  # 创建nginx目录
RUN cd nginx-1.8.0 && ./configure --prefix=/usr/local/nginx && make && make install  # 编译安装
RUN rm -fv /usr/local/nginx/conf/nginx.conf  # 删除自带的nginx配置文件
ADD http://www.apelearn.com/study_v2/.nginx_conf /usr/local/nginx/conf/nginx.conf  # 添加nginx配置文件
# Expose ports  开放80端口出来
EXPOSE 80
# Set the default command to execute when creating a new container  这里是因为防止服务启动后容器会停止的情况,所以需要多执行一句tail命令
ENTRYPOINT /usr/local/nginx/sbin/nginx && tail -f /etc/passwd
#如果你本地的宿主机上,已经有nginx配置文件了,则可以把ADD更改为使用COPY来进行拷贝
COPY /usr/local/nginx/conf/nginx.conf /usr/local/nginx/conf/nginx.conf

多步骤创建

首先创建一段 Go 语言程序源码,仅输出 “Hello” 信息:

[root@server6 ~]$ vi main.go
// main.go will output "Hello, Docker"
package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello!")
}

编写 Dockerfile 文件,将编译和运行合并到一起:

[root@server6 ~]$ vi Dockerfile_builder
FROM golang as builder
RUN mkdir -p /go/src/test
WORKDIR /go/src/test
COPY main.go .
RUN go env -w GO111MODULE=auto
RUN CGO_ENABLED=O GOOS=linux go build -o app .

FROM alpine:latest
WORKDIR /root/
COPY --from=builder /go/src/test/app .
CMD ["./app"]

首先,在一个单独的构建阶段使用 Go 语言编译源码生成可执行文件。然后,在另一个阶段使用 Alpine 镜像作为基础镜像,将编译好的可执行文件复制到容器中,并运行它。

构建并测试运行:

[root@server6 ~]$ docker build -t builder/go_app:1.0.0 -f Dockerfile_builder .
[root@server6 ~]$ docker run --rm builder/go_app:1.0.0
Hello!