使用 Docker 创建镜像

Dockerfile 是一个文本格式的配置文件,风格却像是一段代码,用户可以通过编写 Dockerfile 来创建自定义镜像。

本文将从 Dockerfile 的基本格式入手,罗列其支持的指令并进行介绍,并说明如何通过这些指令来构建我们想要的镜像。最后还会讲下一些编写 Dockerfile 的经验。

1. 基础结构

Docker 由一行行命令语句和以 # 开头的注释语句构成。主体内容分为四个部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。

下面从一个简单的 Dockerfile 例子开始说明:

# escape=\ (backslash)
# This dockerfile uses the ubuntu:xenial image
# VERSION 2 - EDITION 1
# Author: docker_user
# Command format: Instruction [arguments / command] ..

# Base image to use, this must be set as the first line
FROM ubuntu:xenial

# Maintainer: docker_user <docker_user at email.com> (@docker_user)
LABEL maintainer docker_user<docker_user@email.com>

# Command to update the image 
RUN echo "deb http://archive.ubuntu.com/ubuntu/ xenial main universe" >> /etc/apt/sources.list
RUN apt-get update && apt-get install -y nginx
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf

# Command when creating a new container 
CMD /usr/sbin/nginx

1.1 解析器指令

这个例子中,第一行是解析器命令,而不是普通的注释,它指定了这个 Dockerfile 文本的转义字符。

# escape=\ (backslash) 或者 # escape=` (backtick),默认转义字符是 \ 。

转义字符既用于转义一行中的字符,也用于转义换行符。这使得一条 Dockerfile 指令可以跨越多行。注意,无论解析器指令 escape 是否包含在 Dockerfile 中,转义都不会在 RUN 指令中执行,除非要转义的字符出现在行末尾。

在Windows环境下,将转义字符设置为反引号而不是 \ 将非常有用,这样,\ 就可以保持其作为目录路径分隔符的原有的含义,与 Windows PowerShell 中保持一致。

考虑下面的这个例子,它在Windows上可能会非常诡异地执行失败。第二行末尾的第二个 \ 将被视为用来转义换行符的转义字符,而不是第一个 \ 的转义目标。类似地,假设第三行末尾的 \ 本来是要作为一条指令进行处理的,但是它却将被视为行延续字符。这个 Dockerfile 的结果是,第二行和第三行被认为是一条指令:

FROM microsoft/nanoserver
COPY testfile.txt c:\\
RUN dir c:\

解决方式就是指定转义字符为 `

更多关于解析器指令的资料可以参考 Docker 官方文档

1.2 主体部分

一般而言,大部分镜像会包含一个 FROM 标签指定基础镜像信息,即要构建的新镜像是基于哪个旧镜像构建的。接下来是使用 LABEL 指令说明维护者信息,后面则是镜像操作指令,需要注意的是每运行一条 RUN 指令,镜像添加新的一层,并提交,最后是 CMD 指令,指定容器启动时的操作命令。

2. 指令说明

Dockerfile 中指令的一般格式是 INSTRUCTION arguments,类型有配置指令(配置镜像信息)和操作指令(具体执行操作)。

2.1 配置指令

2.1.1 ARG

定义了创建镜像过程中的变量。
格式为 ARG <name>[=<default value>]。
在执行 docker build 时,可以通过 -build-arg[=] 来为变量赋值。当镜像编译成功后, ARG 变量将不再存在(ENV 变量将一直保留在环境变量中)。
Docker 中内置了一些镜像创建变量,可以直接使用(无需区分大小写):HTTP_PROXY、hTTPS_PROXY、FTP_PROXY、NO_PROXY。

2.1.2 FROM

指定所创建镜像的基础镜像。
格式为 FROM <image> [AS <name>] 或 FROM <image>:<tag> [AS <name>] 或 FROM <image>@<digest> [AS <name>]。
任何 Dockerfile 的第一条指令(不算解析器指令),必须是 FROM 指令。如果在同一个 Dockerfile 中有多个镜像,可以使用多个 FROM 指令。

例子为

ARG VERSION=9.3
FROM debian:${VERSION}

2.1.3 LABEL

LABEL 指令可以为生成的镜像添加元数据标签信息。之后可以使用 docker ps -f lable key=value 快速过滤出特定镜像。
格式为 LABEL <key>=<value> <key>=<value> <key>=<value>....

例如:

LABEL author="wean" date="2019-1-16"
LABEL description="This is a mult-line \
    text"

2.1.4 EXPOSE

声明镜像内服务监听的端口。
格式为 EXPOSE <port> [<port>/<protocol>...]
例如:

EXPOSE 22 80 443

需要注意的是,该指令只是起到声明的作用,并不会自动完成端口映射。
如果要映射端口出来,在启动容器时可以使用 -P 或 -p 参数,可以参考 操作 Docker 容器,查看端口映射的具体操作。

2.1.5 ENV

指定环境变量,在编译镜像过程中会被后续 RUN 指令使用,在镜像启动后也会存在于环境变量中。
例如:

ENV APP_VERSION=1.0.0
ENV PATH $PATH:/usr/local/bin

指令指定的环境变量在启动时也可以覆盖掉:docker run --env <key>=<value> built_image

注意当一条 ENV 指令中同时为多个环境变量赋值并且值也是从环境变量读取时;会为变量先赋值后更新。如下面指令,最终结果是 key1=value1 key2=value2

ENV key1=value2
ENV key1=value1 key2=${key1}

2.1.6 ENTRYPOINT

指定镜像的默认入口命令,该命令会在启动容器时作为跟命令执行,所有传入值作为该命令的参数。
支持两种格式:

  • ENTRYPOINT ["executable", "param1", "param2"]: exec 调用执行
  • ENTRYPOINT command param1 param2: shell 调用执行。
    此时,若是指定了 CMD 指令或者 docker run 指定了启动命令,则会作为 ENTRYPOINT 根命令的参数。
    每个 Dockerfile 中只能有一个 ENTRYPOINT,当指定多个时,只有最后一个生效。
    在运行时,可以被 --entrypoint 参数覆盖掉。

关于 ENRTYPOINT 和 CMD 的更多差异可以参考 https://www.cnblogs.com/lienhua34/p/5170335.html

2.1.7 VOLUME

创建数据卷挂载点,挂载到本地的一个随机目录下。
格式为 VOLUME ["/data"]。

2.1.8 USER

指定运行容器时的用户名或者 UID,后续的 RUN 指令也会默认使用特定的用户身份。
格式为 USER daemon。
当服务不需要管理员权限时,可以在 Dockerfile 中创建需要所需要的用户,并通过 USER 命令指定该运行用户。例如:

RUN groupadd -r postgres && useradd --no-log-init -r -g postgress postgress

后续需要获取临时管理员权限可以使用 gosu 命令。

2.1.9 WORKDIR

为后续的 RUN、CMD、ENTRYPOINT 指令配置工作目录。
格式为 WORKDIR /path/to/workdir
可以使用多个 WORKDIR 指令,但是后续指令如果是相对路径,基于之前的指令指定的路径。
例如:

WORKDIR /a
WORKDIR b

结果是路径 /a/b,所以推荐要是需要写多个 WORKDIR,则每个 WORKDIR 都写绝对路径。

2.1.10 ONBUILD

指定当基于所生成的镜像创建子镜像时,自动执行的操作指令。
格式为 ONBUILD [INSTRUCTION]
由由于这个命令是隐式执行的,子镜像创建过程中察觉不到,所以应该在镜像标签中进行标注,例如 ruby:2.1-onbuild。
这个指令在创建专门用于自动编译、检查等操作的基础镜像时十分有用。

2.1.11 STOPSIGNAL

默认的stop-signal是SIGTERM,在docker stop的时候会给容器内PID为1的进程发送这个signal,通过--stop-signal可以设置自己需要的signal,主要的目的是为了让容器内的应用程序在接收到signal之后可以先做一些事情,实现容器的平滑退出,如果不做任何处理,容器将在一段时间之后强制退出,会造成业务的强制中断,这个时间默认是10s。
格式为 STOPSIGNAL signal

2.1.12 HEALTHCHECK

配置所启动容器如何进行健康检查(如何判断健康与否)。格式有两种:

  • HEALTHCHECK [OPTIONS] CMD command:根据所执行命令返回值是否为 0 来判断
  • HEALTHCHECK NONE: 禁止基础镜像中的健康检查

OPTIONS 支持如下参数:

  • -interval=DURATION (default: 30s): 过多久检查一次。
  • -timeout=DURATION (default: 30s): 每次检查的超时时间。
    • retries=N (default: 3): 如果失败了,重试几次才最终确定失败。

2.1.13 SHELL

指定其他命令使用 shell 时的默认 shell 类型。
SHELL ["executable", "parameters"]。
默认值是 ["/bin/sh", "-c"]。

2.2 操作指令

2.2.1 RUN

运行指定命令。
格式为 RUN <command> 或 RUN ["executable", "param1", "param2"]。命令过长可以使用转义字符换行,比如默认的
每条 RUN 指令会生成一个新的镜像层。

2.2.2 CMD

CMD 容器用来指定容器启动时默认执行的命令。支持三种格式:

  • CMD ["executable", "param1", "param2"]: 相当于执行 executable param1 param2,推荐方式
  • CMD command parame1 param2: 在默认的 shell 中执行,提供给需要交互的应用
  • CMD ["param1", "param2"]: 提供给 ENTRYPOINT 的默认参数
    每个镜像只能有一个 CMD,多条会覆盖,可以在运行 (run) 容器时通过参数覆盖

2.2.3 ADD

添加内容到镜像。格式为 ADD <src> <dest>。
该命令复制指定的 <src> 下的内容到容器中的 <dest> 路径下。
其中 <src> 可以是 Dockerfile 所在目录的一个相对路径(文件或目录);也可以是一个 URL;还可以是一个 tar 文件(会自动解压为目录)。<dest> 可以是镜像内的绝对路径,或者相对于工作目录 WORKDIR 下的相对路径。且路径支持正则格式。

2.2.4 COPY

COPY 指令与 ADD 相似,语义上 CPOY 更贴切,推荐使用 COPY。

3. 创建镜像

编写完 Dockerfile 后就可以通过 docker [image] build [OPTIONS] PATH|URL|- 命令来创建镜像。
要注意的点有:

  • 碰到 ADD、COPY 和 RUN 指令会生成一层新的镜像
  • 执行命令的过程中会把 Dockerfile 文件所在的上下文都发给服务端。因此应该避免无关的文件存在上下文,或者使用 .dockerignore 文件。
  • 非上下文路径的 Dockerfile 使用 -f 选项来指定其路径
  • 要指定生成镜像的标签信息,可以使用 -t 选项。该选项可以重复使用多次来添加多个标签。

3.1 OPTIONS 命令选项

选项 说明
-add-host list 添加自定义的主机名到 IP 的映射
-build-arg list 添加创建时的变量
-cache-from strings 使用指定镜像作为缓存源
-cgroup-parent string 继承上层 cgroup
-compress 使用 gzip 来压缩创建上下文数据
-cpu-period int 分配的 CFS 调度器时长
-cpu-quota int CPU 调度器总份额
-c, -cpu-shares int CPU 权重
-cpuset-cpus string 多 CPU 允许使用的 CPU
-cpuset-mems string 多 CPU 允许使用的内存
-disable-content-trust 不进行镜像校验,默认为真
-f,-file string Dockerfile 名称
-force-rm 总是删除中间过程的容器
-iidfile string 将镜像 ID 写入到文件
-isolation string 容器的隔离机制
-label list 配置镜像的元数据
-m,-memory bytes 限制使用内存量
-memory-swap bytes 限制内存和缓存的总量
-network string 指定 RUN 命令时的网络模式
-no-cache 创建镜像时不适用缓存
-platform string 指定平台类型
-pull 总是尝试获取镜像的最新版本
-q,-quiet 不打印创建过程中的日志信息
-rm 创建成功后自动删除中间过程容器,默认为真
-security-opt strings 指定安全相关的选项
-shm-size bytes /dev/shm 的大小
-squash 将新创建的多层挤压放到一层中
-stream 持续获取创建的上下文
-t,-tag list 指定镜像的标签列表
-target string 指定创建的目标阶段
-ulimit ulimit 指定 ulimit 变量

3.2 选择父镜像

镜像的继承关系

从这个镜像的继承图来看,我们可以选择两种镜像作为父镜像,一种是基础镜像(灰色),另一种是普通镜像(白色)。

3.3 使用 .dockerignore 文件

从前面我们可以知道,Dockerfile build 的过程中会把上下文一起发给服务端,如果有不想发送的文件(加快传输过程),可以使用一下模式忽略文件:

  • /temp
  • //temp*
  • tmp?
  • ~*
  • Dockerfile
  • !README.md

其中 * 表示任意多的字符, ? 代表单个字符, ! 代表不匹配(即不忽略指定的路径或文件)。

3.4 多步骤创建

从 17.05 版本开始,Docker 支持多步骤镜像创建,用来精简最终生成的镜像的大小。

举个简单的例子,制作一个 Java 程序镜像,分成两个步骤创建,第一步用包含 maven 库及 JDK 的编译镜像编译程序,第二部把生成的 jar 文件复制到运行镜像中,打包运行镜像就得到精简的镜像。

以 Go 语言为例,提供一个例子:

package main 

import (
    "fmt"
)

func main() {
    fmt.Println("Hello, Docker")
}
FROM golang:1.9 as builder
RUN mkdir -p /go/src/test
WORKDIR /go/src/test
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR/root/
COPY --from=builder /go/src/test/app .
CMD ["./app"]

编译镜像,可以看到最终大小只有 6.83 MB

>docker build -t wean2018/test-multistage:latest .

> docker images
REPOSITORY                                      TAG                 IMAGE ID            CREATED             SIZE
wean2018/test-multistage                        latest              2e2eed03a664        53 seconds ago      6.83MB
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容