Docker笔记-07 Dockerfile

Dockerfile 是一个文本格式的配置文件, 我们可以使用 Dockerfile 来快速创建自定义的镜像。

1 基本结构

Dockerfile由一行行命令语句组成,并且支持以#开头的注释行。

一般而言,Dockerfile分为四部分:

  • 基础镜像信息
  • 维护者信息
  • 镜像操作指令
  • 容器启动时执行指令
#第一行必须指定基于的基础镜像
FROM ubuntu:xeniel

#维护者信息
MAINTAINER docker_user  docker_user@email.com

#镜像的操作指令
RUN echo "deb http://archive.ubuntu.com/ubuntu/ raring main universe ">> /etc/apt/sources.list
RUN apt-get update && apt-get install -y nginx
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf

#容器启动时执行指令
CMD /usr/bin/nginx

每运行一条RUN指令,镜像添加新的一层,并提交。最后CMD指令,来指定运行容器时的操作命令。

1.1 nginx的Dockerfile

debian:jessie基础镜像基础上安装Nginx环境, 从而创建一个新的nginx镜像

FROM debian:jessie 

LABEL maintainer docker_user<docker_user@email.com> 

ENV NGINX VERSION 1.10.1-1-jessie 

RUN apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3DBFBC641079A6ABABF5BDB27BD9BF62 \ 
        && echo "deb http://nginx.org/packages/debian/ jessie nsuginx" >> /etc/apt/sources.list \
        && apt-get update \
        && apt-get install --no-install-recommends --no-install-suggests -y \ 
        ca-certificates \
        nginx=${NGINX_VERSION} \ 
        nginx-module-xslt \
        nginx-module-geoip \ 
        nginx-module-image-filter \
        nginx-module-perl \ 
        nginx-module-njs \ 
        gettext-base \
       && rm -rf /var/lib/apt/lists/*
       
# forward request and error logs to docker log callector
RUN ln -sf /dev/stdout /var/log/nginx/access.log \ 
   && ln -sf /dev/stderr /var/log/nginx/error.log
   
EXPOSE 80 443

CMD ["nginx", "-g", "daemon off;"]

1.2 Go的Dockerfile

基于buildpack-deps:jessie-scm基础镜像,安装Golang相关环境, 制作一个Go语言的运行环境镜像。

FROM buildpack-deps:jessie-scm 

# gee for ego

RUN apt-get update && apt-get install -y --no-install-recommends \
    g++ \ 
    gcc \ 
    libe6-dev \ 
    make \ 
    && rm -rf /var/lib/apt/lists/*

    ENV GOLANG VERSION 1. 6. 3 
    ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz
    ENV GOLANG DOWNLOAD SHA256 cdde5e08530c0579255d6153b08fdb3b8e47caabbe717bc7bcd7561275a87aeb 

    RUN curl -fsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz \ 
        && echo "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c -\  
        &&tar -c /usr/local -xzf golang.tar.gz \ 
        && rm golang.tar.gz 

    ENV GOPATH /go 
    ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH 

    RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH" 
    WORKDIR $GOPATH 

    COPY go-wrapper /usr/local/bin/

2 指令说明

指令的一般格式为INSTRUCTION arguments,指令包括"配置指令(配置镜像信息)“和"操作指令(具体执行操作)”。

分类指令说明
配置指令ARG定义创建镜像过程中使用的变量
配置指令FROM指定所创建镜像的基础镜像
配置指令LABEL为生成的镜像添加元数据标签信息
配置指令EXPOSE声明镜像内服务监听的端口
配置指令ENV指定环境变量
配置指令ENTRYPOINT指定镜像的默认入口命令
配置指令VOLUME创建一个数据卷挂载点
配置指令USER指定运行容器时的用户名或UID
配置指令WORKDIR配置工作目录
配置指令ONBUILD创建子镜像时指定自动执行的操作指令
配置指令STOPSIGNAL指定退出的信号值
配置指令HEALTHCHECK配置所启动容器如何进行健康检查
配置指令SHELL指定默认shell类型
操作指令RUN运行指定命令
操作指令CMD启动容器时指定默认执行的命令
操作指令ADD添加内容到镜像
操作指令COPY复制内容到镜像

2.1 配置指令

2.1.1 ARG

定义创建镜像过程中使用的变量。格式为

ARG <name>[=<default value>]

在执行docker build时, 可以通过-build-arg[=]来为变量赋值。

ARGENV的效果一样,都是设置环境变量。所不同的是,ARG所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用ARG保存密码之类的信息,因为docker history还是可以看到所有值的。

灵活的使用ARG指令,能够在不修改Dockerfile的情况下,构建出不同的镜像。

ARG指令有生效范围,如果在FROM指令之前指定,那么只能用于FROM指令中。

ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

RUN set -x ; echo ${DOCKER_USERNAME}

使用上述Dockerfile会发现无法输${DOCKER_USERNAME}变量的值,要想正常输出,你必须在FROM之后再次指定ARG

# 只在 FROM 中生效
ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

# 要想在 FROM 之后使用,必须再次指定
ARG DOCKER_USERNAME=library

RUN set -x ; echo ${DOCKER_USERNAME}

对于多阶段构建,尤其要注意这个问题

# 这个变量在每个 FROM 中都生效
ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

RUN set -x ; echo 1

FROM ${DOCKER_USERNAME}/alpine

RUN set -x ; echo 2

对于上述Dockerfile两个FROM指令都可以使用${DOCKER_USERNAME},对于在各个阶段中使用的变量都必须在每个阶段分别指定:

ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library

RUN set -x ; echo ${DOCKER_USERNAME}

FROM ${DOCKER_USERNAME}/alpine

# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library

RUN set -x ; echo ${DOCKER_USERNAME}

Docker内置了一些镜像创建变量,用户可以直接使用而无须声明,包括(不区分大小写)HTTP_PROXYHTTPS_PROXYFTP_PROXYNO_PROXY

2.1.2 FROM

指定所创建镜像的基础镜像。格式为:

FROM <image> [AS <name>] 
或 
FROM <image>:<tag> [AS <name>] 
或
FROM <image>@<digest> [AS <name>] 。

任何Dockerfile中第一条指令必须为FROM指令。并且,如果在同一个Dockerfile中创建多个镜像时,可以使用多个FROM 指令(每个镜像一次)。

为了保证镜像精简,可以选用体积较小的镜像如Alpine或Debian作为基础镜像。 例如:

ARG VERSION=9.3 
FROM debian:${VERSION}

在Docker Hub上有非常多的高质量的官方镜像:

  • 有可以直接拿来使用的服务类的镜像,如nginxredismongomysqlhttpdphptomcat等;
  • 也有一些方便开发、构建、运行各种语言应用的镜像,如nodeopenjdkpythonrubygolang等。

可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。

如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如ubuntudebiancentosfedoraalpine等,这些操作系统的软件库为我们提供了更广阔的扩展空间。

除了选择现有镜像为基础镜像外,Docker还存在一个特殊的镜像,名为scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

FROM scratch
...

如果你以scratch为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于Linux下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接FROM scratch会让镜像体积更加小巧。使用Go语言开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为Go是特别适合容器微服务架构的语言的原因之一。

2.1.3 LABEL

LABEL指令可以为生成的镜像添加元数据标签信息。 这些信息可以用来辅助过滤出特定镜像。格式为:

LABEL <key>=<value> <key>=<value> <key>=<value> ...

例如:

LABEL version="l.0.0-rc3" 
LABEL author="yeasy@github" date="2020-01-01" 
LABEL description="This text illustrates \
    that label-values can span multiple lines."

具体可以参考https://github.com/opencontainers/image-spec/blob/master/annotations.md

2.1.4 EXPOSE

声明镜像内服务监听的端口。格式为:

EXPOSE <port> [<port>/<protocol>... ]

例如:

EXPOSE 22 80 8443 

注意该指令只是起到声明作用,并不会自动完成端口映射。如果要映射端口出来,在启动容器时可以使用-P参数(Docker主机会自动分配一个宿主机的临时端口)或-p HOST_PORT:CONTAINER_PORT参数(具体指定所映射的本地端口)。

2.1.5 ENV

指定环境变量,在镜像生成过程中会被后续RUN指令使用,在镜像启动的容器中也会存在。格式为:

ENV <key> <value>
或
ENV <key1>=<value1> <key2>=<value2>...

例如:

ENV APP VERSION=1.0.0
ENV APP_HOME=/usr/local/app 
ENV PATH $PATH:/usr/local/bin 

指令指定的环境变量在运行时可以被覆盖掉, 如

docker run --env <key>=<value> built_image

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

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

下列指令可以支持环境变量展开:ADDCOPYENVEXPOSEFROMLABELUSERWORKDIRVOLUMESTOPSIGNALONBUILDRUN

2.1.6 ENTRYPOINT

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

  • ENTRYPOINT ["executable", "param1", "param2"]: exec调用执行;
  • ENTRYPOINT command param1 param2: shell中执行。

ENTRYPOINT的目的和CMD一样,都是在指定容器启动程序及参数。ENTRYPOINT在运行时也可以替代,不过比CMD要略显繁琐,需要通过docker run的参数--entrypoint来指定。

当指定了ENTRYPOINT后,CMD的含义就发生了改变,不再是直接的运行其命令,而是将CMD的内容作为参数传给ENTRYPOINT指令,换句话说实际执行时,将变为:

<ENTRYPOINT> "<CMD>"

那么有了CMD后,为什么还要有ENTRYPOINT呢?这种<ENTRYPOINT> "<CMD>"有什么好处么?让我们来看几个场景。

  • 场景一:让镜像变成像命令一样使用

假设我们需要一个得知自己当前公网I 的镜像,那么可以先用CMD来实现:

FROM ubuntu:18.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://myip.ipip.net" ]

假如我们使用docker build -t myip . 来构建镜像的话,如果我们需要查询当前公网IP,只需要执行:

$ docker run myip
当前 IP:61.148.226.66 来自:北京市 联通

嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的CMD中可以看到实质的命令是curl,那么如果我们希望显示HTTP头信息,就需要加上-i参数。那么我们可以直接加-i参数给docker run myip么?

$ docker run myip -i
docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".

我们可以看到可执行文件找不到的报错,executable file not found。之前我们说过,跟在镜像名后面的是command,运行时会替换CMD的默认值。因此这里的-i替换了原来的 CMD,而不是添加在原来的curl -s http://myip.ipip.net后面。而-i根本不是命令,所以自然找不到。

那么如果我们希望加入-i这参数,我们就必须重新完整的输入这个命令:

$ docker run myip curl -s http://myip.ipip.net -i

这显然不是很好的解决方案,而使用ENTRYPOINT就可以解决这个问题。现在我们重新用ENTRYPOINT来实现这个镜像:

FROM ubuntu:18.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]

这次我们再来尝试直接使用docker run myip -i

$ docker run myip
当前 IP:61.148.226.66 来自:北京市 联通

$ docker run myip -i
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 22 Nov 2016 05:12:40 GMT
Content-Type: text/html; charset=UTF-8
Vary: Accept-Encoding
X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
X-Cache: MISS from cache-2
X-Cache-Lookup: MISS from cache-2:80
X-Cache: MISS from proxy-2_6
Transfer-Encoding: chunked
Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
Connection: keep-alive

当前 IP:61.148.226.66 来自:北京市 联通

可以看到,这次成功了。这是因为当存在ENTRYPOINT后,CMD的内容将会作为参数传给ENTRYPOINT,而这里-i就是新的 CMD,因此会作为参数传给curl,从而达到了我们预期的效果。

  • 场景二:应用运行前的准备工作

启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。比如mysql类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的mysql服务器运行之前解决。

此外,可能希望避免使用root用户去启动服务,从而提高安全性,而在启动服务前还需要以root身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用root身份执行,方便调试等。

这些准备工作是和容器CMD无关的,无论CMD做什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入ENTRYPOINT中去执行,而这个脚本会将接到的参数(也就是<CMD>)作为命令,在脚本最后执行。比如官方镜像 redis中就是这么做的:

FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD [ "redis-server" ]

可以看到其中为了redis服务创建了redis用户,并在最后指定了ENTRYPOINTdocker-entrypoint.sh脚本。

#!/bin/sh
...
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
	find . \! -user redis -exec chown redis '{}' +
	exec gosu redis "$0" "$@"
fi

exec "$@"

该脚本的内容就是根据CMD的内容来判断,如果是redis-server的话,则切换到redis用户身份启动服务器,否则依旧使用root身份执行。比如:

$ docker run -it redis id
uid=0(root) gid=0(root) groups=0(root)

另外,每个Dockerfile中只能有一个ENTRYPOINT, 当指定多个时,只有最后一个起效。

2.1.7 VOLUME

创建一个数据卷挂载点。格式为:

VOLUME ["<路径1>", "<路径2>"...]
或
VOLUME <路径>

运行容器时可以从本地主机或其他容器挂载数据卷,一般用来存放数据库和需要保持的数据等。

容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在Dockerfile中,可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

VOLUME /data

这里的/data目录就会在容器运行时自动挂载为匿名卷,任何向/data中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行容器时可以覆盖这个挂载设置。比如:

$ docker run -d -v mydata:/data xxxx

在这行命令中,就使用了mydata这个命名卷挂载到了/data这个位置,替代了Dockerfile中定义的匿名卷的挂载配置。

2.1.8 USER

指定运行容器时的用户名或UID, 后续的RUN等指令也会使用指定的用户身份。格式为:

USER <用户名>[:<用户组>]

USER指令和WORKDIR相似,都是改变环境状态并影响以后的层。WORKDIR是改变工作目录,USER则是改变之后层的执行 RUN, CMD以及ENTRYPOINT这类命令的身份。

注意,USER只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

如果以root执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用su或者sudo,这些都需要比较麻烦的配置,而且在TTY缺失的环境下经常出错。建议使用gosu

# 建立 redis 用户,并使用 gosu 换另一个用户执行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \
    && chmod +x /usr/local/bin/gosu \
    && gosu nobody true
# 设置 CMD,并以另外的用户执行
CMD [ "exec", "gosu", "redis", "redis-server" ]

2.1.9 WORKDIR

为后续的RUN,CMD,ENTRYPOINT指令配置工作目录,以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR会帮你建立目录。格式为:

WORKDIR path /to/workdir

可以使用多个WORKDIR令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如

WORKDIR /a 
WORKDIR b 
WORKDIR c 
RUN pwd

则最终路径为/a/b/c。因此,为了避免出错,推荐WORKDIR指令中只使用绝对路径。

2.1.10 ONBUILD

指定当基于所生成镜像创建子镜像时,自动执行的操作指令。格式为

ONBUILD [INSTRUCTION]

ONBUILD是一个特殊的指令,它后面跟的是其它指令,比如RUN, COPY等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。

Dockerfile中的其它指令都是为了定制当前镜像而准备的,唯有ONBUILD是为了帮助别人定制自己而准备的。

例如,使用如下的Dockerfile创建父镜像ParentImag ,指定ONBUILD指令

# Dockerfile for Parentimage 
[...] 
ONBUILD ADD . /app/src 
ONBUILD RUN /usr/local/bin/python-build --dir /app/src 
[...] 

使用docker build命令创建子镜像ChildImage时(FROM Parentimage),会首先执行ParentImage配置的ONBUILD指令:

# Dockerfile for Childimage 
FROM Parenti mage

等价于在Childimage Dockerfile 中添加了如下指令:

#Automatically run the following when building Childimage
ADD . /app/src 
RUN /usr/local/bin python-buld --dir /app/src

由于ONBUILD指令是隐式执行的,推荐在使用它的镜像标签中进行标注,例如ruby:2.l-onbuild。ONBUILD指令在创建专门用于自动编译、检查等操作的基础镜像时,十分有用。

2.1.11 STOPSIGNAL

指定所创建镜像启动的容器接收退出的信号值

STOPSIGNAL signal

2.1.12 HEALTHCHECK

配置所启动容器如何进行健康检查(如何判断健康与否),自Docker 1.12开始支持。格式有两种:

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

OPTION支持如下参数:

  • -interval=<间隔> :两次健康检查的间隔,默认为30秒;
  • -timeout=<时长> : 健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认30秒;
  • -retries=<次数> :当连续失败指定次数后,则将容器状态视为unhealthy,默认3次。。

在没有HEALTHCHECK指令前,Docker引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。在1.12以前,Docker不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。

而自1.12之后,Docker提供了HEALTHCHECK指令,通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。

当在一个镜像指定了HEALTHCHECK指令后,用其启动容器,初始状态会为starting,在HEALTHCHECK指令检查成功后变为healthy,如果连续一定次数失败,则会变为unhealthy

CMD, ENTRYPOINT一样,HEALTHCHECK只可以出现一次,如果写了多个,只有最后一个生效。

HEALTHCHECK [选项] CMD后面的命令,格式和ENTRYPOINT一样,分为shell格式,和exec格式。命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留,不要使用这个值。

假如有个镜像是个最简单的Web 服务,希望增加健康检查来判断Web服务是否在正常工作,可以用curl来帮助判断,其Dockerfile的HEALTHCHECK可以这么写:

FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
  CMD curl -fs http://localhost/ || exit 1

这里设置了每5秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过3秒没响应就视为失败,并且使用curl -fs http://localhost/ || exit 1作为健康检查命令。

使用docker build来构建这个镜像:

$ docker build -t myweb:v1 .

构建好了后,我们启动一个容器:

$ docker run -d --name web -p 80:80 myweb:v1

当运行该镜像后,可以通过docker container ls看到最初的状态为 (health: starting):

$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   3 seconds ago       Up 2 seconds (health: starting)   80/tcp, 443/tcp     web

在等待几秒钟后,再次docker container ls,就会看到健康状态变化为了 (healthy):

$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   18 seconds ago      Up 16 seconds (healthy)   80/tcp, 443/tcp     web

如果健康检查连续失败超过了重试次数,状态就会变为 (unhealthy)。

为了帮助排障,健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里,可以用docker inspect来查看。

$ docker inspect --format '{{json .State.Health}}' web | python -m json.tool
{
    "FailingStreak": 0,
    "Log": [
        {
            "End": "2016-11-25T14:35:37.940957051Z",
            "ExitCode": 0,
            "Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n    body {\n        width: 35em;\n        margin: 0 auto;\n        font-family: Tahoma, Verdana, Arial, sans-serif;\n    }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",
            "Start": "2016-11-25T14:35:37.780192565Z"
        }
    ],
    "Status": "healthy"
}

2.1.13 SHELL

SHELL指令可以指定RUN,ENTRYPOINT, CMD指令的shell类型,Linux中默认为["/bin/sh", "-c"]

SHELL ["executable","parameters"] 

示例:

SHELL ["/bin/sh", "-c"]

RUN lll ; ls

SHELL ["/bin/sh", "-cex"]

RUN lll ; ls

两个RUN运行同一命令,第二个RUN运行的命令会打印出每条命令并当遇到错误时退出。

ENTRYPOINT, CMD以shell格式指定时,SHELL指令所指定的shell也会成为这两个指令的shell:

SHELL ["/bin/sh", "-cex"]

# /bin/sh -cex "nginx"
ENTRYPOINT nginx
SHELL ["/bin/sh", "-cex"]

# /bin/sh -cex "nginx"
CMD nginx

2.2 操作指令

2.2.1 RUN

运行指定命令。格式为:

  • shell格式:RUN <命令> 默认将在shell终端中运行命令,即/bin/sh -c
RUN <comand> 
# 例如
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式,使用exec执行。指令会被解析为JSON数组,因此必须用双引号。
RUN ["executable", "param1", "param2"]

指定使用其他终端类型可以通过第二种方式实现,例如RUN ["/bin/bash", "-c","echo hello"]

每条RUN指令将在当前镜像基础上执行指定命令,并提交为新的镜像层。当命令较长时可以使用\来换行。例如:

RUN apt-get update \ 
    && apt-get install -y libsnappy-dev zliblg-dev libbz2-dev \ 
    && rm -rf /var/cache/apt \
    && rm rf /var/lib/apt/lists/*
  • 层数限制
    有些时候RUN命令会写为这样:
FROM debian:stretch

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install

之前说过,Dockerfile中每一个指令都会建立一层,RUN也不例外。每一个RUN的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit这一层的修改,构成新的镜像。

而上面的这种写法,创建了7层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学Docker的人常犯的一个错误。

UnionFS是有最大层数限制的,比如AUFS,曾经是最大不得超过42层,现在是不得超过127层。上面的Dockerfile正确的写法应该是这样:

FROM debian:stretch

RUN set -x; buildDeps='gcc libc6-dev make wget' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

首先,之前所有的命令只有一个目的,就是编译、安装redis可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个RUN一一对应不同的命令,而是仅仅使用一个RUN指令,并使用&&将各个所需命令串联起来。将之前的7层,简化为了1层。在撰写Dockerfile的时候,要经常提醒自己,这并不是在写Shell脚本,而是在定义每一层该如何构建。

并且,这里为了格式化还进行了换行。Dockerfile支持Shell类的行尾添加\的命令换行方式,以及行首#进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。

此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了apt缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

很多人初学Docker制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

2.2.2 CMD

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

  • CMD ["executable","param1","param2"] 使用exec执行,推荐方式。
  • CMD command param1 param2 在默认的Shell(/bin/sh)中执行,提供给需要交互的应用。
  • CMD ["param1","param2"] 提供给ENTRYPOINT的默认参数,在指定了ENTRYPOINT指令后,用CMD指定具体的参数。

Docker不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD指令就是用于指定默认的容器主进程的启动命令的。

在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu镜像默认的CMD/bin/bash,如果我们直接 docker run -it ubuntu的话,会直接进入bash。我们也可以在运行时指定运行别的命令,如docker run -it ubuntu cat /etc/os-release。这就是用cat /etc/os-release命令替换了默认的/bin/bash命令了,输出了系统版本信息。

在指令格式上,一般推荐使用exec格式,这类格式在解析时会被解析为JSON数组,因此一定要使用双引号",而不要使用单引号。

如果使用shell格式的话,实际的命令会被包装为sh -c的参数的形式进行执行。比如:

CMD echo $HOME

在实际执行中,会将其变更为:

CMD [ "sh", "-c", "echo $HOME" ]
  • 前台后台执行
    提到CMD就不得不提容器中应用在前台执行和后台执行的问题。Docker不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用systemd去启动后台服务,容器内没有后台服务的概念。

如果如下面写为:

CMD service nginx start

然后发现容器执行后就立即退出了。甚至在容器内去使用systemctl命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

而使用service nginx start命令,则是希望upstart来以后台守护进程形式启动nginx服务。而刚才说了CMD service nginx start会被理解为CMD [ "sh", "-c", "service nginx start"],因此主进程实际上是sh。那么当service nginx start命令结束后,sh也就结束了,sh作为主进程退出了,自然就会令容器退出。

正确的做法是直接执行nginx可执行文件,并且要求以前台形式运行。比如:

CMD ["nginx", "-g", "daemon off;"]

每个Dockerfile只能有一条CMD命令 如果指定了多条命令,只有最后一条会被执行。

如果用户启动容器时候手动指定了运行的命令(作为run命令的参数),则会覆盖掉CMD指定的命令。

2.2.3 ADD

添加内容到镜像。格式为:

ADD <src> <dest>

该命令将复制指定的<src>路径下内容到容器中的<dest>路径下。其中

  • <src>可以是Dockerfile所在目录的一个相对路径(文件或目录);也可以是一个URL;还可以是一个tar文件(自动解压为目录)
  • <dest>可以是镜像内绝对路径,或者相对于工作目录(WORKDIR)的相对路径。

路径支持正则格式,例如:

ADD *.c /code/

ADD指令和COPY的格式和性质基本一致。但是在COPY基础上增加了一些功能。

比如<源路径>可以是一个URL,这种情况下,Docker引擎会试图去下载这个链接的文件放到<目标路径>去。下载后的文件权限自动设置为600,如果这并不是想要的权限,那么还需要增加额外的一层RUN进行权限调整。

另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层RUN指令进行解压缩。所以不如直接使用RUN指令,然后使用wget或者curl工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。

如果<源路径>为一个tar压缩文件的话,压缩格式为gzip, bzip2以及xz的情况下,ADD指令将会自动解压缩这个压缩文件到 <目标路径>去。在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu 中:

FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...

在使用该指令的时候还可以加上--chown=<user>:<group>选项来改变文件的所属用户及所属组。

ADD --chown=55:mygroup files* /mydir/
ADD --chown=bin files* /mydir/
ADD --chown=1 files* /mydir/
ADD --chown=10:11 files* /mydir/

另外需要注意的是,ADD指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。

2.2.4 COPY

复制内容到镜像。和RUN指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。格式为:

COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src1>",... "<dest>"]

COPY指令将从构建上下文目录中<src>的文件/目录复制到新的一层的镜像内的<dest>位置。目标路径不存在时,会自动创建。例如:

COPY package.json /usr/src/app/

<src>可以是多个,甚至可以是通配符,其通配符规则要满足Go的filepath.Match规则,如:

COPY hom* /mydir/
COPY hom?.txt /mydir/

<dest>可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用WORKDIR指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

此外,还需要注意一点,使用COPY指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用Git进行管理的时候。

在使用该指令的时候还可以加上--chown=<user>:<group>选项来改变文件的所属用户及所属组。

COPY --chown=55:mygroup files* /mydir/
COPY --chown=bin files* /mydir/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/

如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。

在Docker官方的Dockerfile最佳实践文档中文翻译)中要求,尽可能的使用 COPY,因为COPY的语义很明确,就是复制文件而已,而ADD则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD的场合,就是所提及的需要自动解压缩的场合。

3 创建镜像

编写完成Dockerfile之后,可以通过docker [image] build命令来创建镜像。基本格式为:

docker build [OPTIONS] PATH |URL| -

该命令将读取指定路径下(包括子目录)的Dockerfile,并将该路径下所有数据作为上下文( Context )发送给 Docker 服务端。Docker服务端在校验Dockerfile格式通过后,逐条执行其中定义的指令,碰到ADD,COPY,RUN指令会生成一层新的镜像。最终如果创建镜像成功,会返回最终镜像的ID。

如果上下文过大, 会导致发送大量数据给服务端,延缓 建过程 除非是生成镜像
所必需的文件,不然不要放到上下文路径 如果使用非上下文路径下的 Dockerfi ,可以
通过 选项来指定其路径
要指定生成镜像的标签信息,可以通过 - t 选项 该选项可以重复使用多次为镜像一次添
加多个名称
例如,上下文路径为/tmp/ ocke builder/,并且希望生成镜像标签为 builder first image: 1.0.0,
可以使用下面的命令
$ docker bu ild -t builder/first_image :l.0.0 /tmp/docker_builder/

3.1 示例

Dockerfile为:

(base) pang@pang-HP:~/dockerfile$ pwd
/home/pang/dockerfile
(base) pang@pang-HP:~/dockerfile$ cat Dockerfile 
FROM nginx:alpine
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
(base) pang@pang-HP:~/dockerfile$ 

创建镜像

(base) pang@pang-HP:~/dockerfile$ sudo docker build -t nginx:v3 . 
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM nginx:alpine
 ---> f246e6f9d0b2
Step 2/2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in 0c824d448972
Removing intermediate container 0c824d448972
 ---> fa48f150ff54
Successfully built fa48f150ff54
Successfully tagged nginx:v3
(base) pang@pang-HP:~/dockerfile$ sudo docker images
REPOSITORY        TAG            IMAGE ID       CREATED          SIZE
nginx             v3             fa48f150ff54   10 seconds ago   23.5MB
nginx             alpine         f246e6f9d0b2   6 days ago       23.5MB
python            3              763642b839b7   2 weeks ago      55.4MB
debian            stretch-slim   a88d8e7bd33c   4 weeks ago      55.4MB
registry          2              773dbf02e42e   4 weeks ago      24.1MB
sonatype/nexus3   latest         aab1398bb647   5 weeks ago      705MB
centos            7              eeb6ee3f44bd   9 months ago     204MB
(base) pang@pang-HP:~/dockerfile$ 

3.2 镜像构建上下文(Context)

docker build命令最后有一个..表示当前目录,而Dockerfile就在当前目录,而这个路径不能单纯的理解为是Dockerfile的路径,而是上下文路径

3.2.1 build工作原理

首先根据docker build的工作原理。Docker在运行时分为Docker引擎(也就是服务端守护进程)和客户端工具。Docker的引擎提供了一组REST API,被称为Docker Remote API,而如docker命令这样的客户端工具,则是通过这组API与Docker引擎交互,从而完成各种功能。因此,虽然表面上是在本机执行各种docker功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的Docker引擎变得轻而易举。

当进行镜像构建的时候,并非所有定制都会通过RUN指令完成,经常会需要将一些本地文件复制进镜像,比如通过COPY指令、ADD指令等。而docker build命令构建镜像,其实并非在本地构建,而是在服务端,也就是Docker引擎中构建的。那么在这种客户端/服务端的架构中,为了能够让服务端获得本地文件就引入了上下文的概念。

当构建的时候,用户会指定构建镜像上下文的路径,docker build命令得知这个路径后,会将路径下的所有内容打包,然后上传给Docker引擎。这样Docker引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

如果在Dockerfile中这么写:

COPY ./package.json /app/

这并不是要复制执行docker build命令所在的目录下的package.json,也不是复制Dockerfile所在目录下的package.json,而是复制上下文(context)目录下的package.json。

因此,COPY这类指令中的源文件的路径都是相对路径。这也就是COPY ../package.json /app或者COPY /opt/xxxx /app 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。

3.2.2 示例解析

现在就可以理解刚才的命令docker build -t nginx:v3 . 中的这个.,实际上是在指定上下文的目录,docker build命令会将该目录下的内容打包交给Docker引擎以帮助构建镜像。

观察docker build输出,其实已经看到了这个发送上下文的过程:

Sending build context to Docker daemon  2.048kB

理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如在发现COPY /opt/xxxx /app不工作后,于是干脆将Dockerfile放到了硬盘根目录去构建,结果发现docker build执行后,在发送一个几十GB的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让docker build打包整个硬盘,这显然是使用错误。

一般来说,应该会将Dockerfile置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给Docker引擎,那么可以用.gitignore一样的语法写一个.dockerignore,该文件是用于剔除不需要作为上下文传递给Docker引擎的。

3.2.3 Dockerfile文件命名

在默认情况下,如果不额外指定Dockerfile的话,会将上下文目录下的名为Dockerfile的文件作为Dockerfile。这只是默认行为,实际上Dockerfile的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如可以用-f ../Dockerfile.php参数指定某个文件作为 Dockerfile。当然,一般大家习惯性的会使用默认的文件名 Dockerfile,以及会将其置于镜像构建上下文目录中。

另外,要指定生成镜像的标签信息,可以通过-t选项 该选项可以重复使用多次为镜像一次添加多个名称。

3.3 命令选项

docker [image] build令支持一系列的选项,可以调整创建镜像过程的行为。

选项说明
-add-host list添加自定义的主机名到IP的映射
-build-arg list添加创建时的变量
-cache-from strings使用指定镜像作为缓存源
-cgroup-parent string继承的上层cgroup
-compress使用gzip来压缩创建上下文数据
-cpu-period int分配的CFS调度器时长
-cpu-quota intCFS调度都总份额
-c,-cpu-shares intCPU权重
-cpuset-cpus string多CPU允许使用的CPU
-cpuset-mems string多CPU允许使用的内存
-disable-content-trust不进行镜像校验,默认为真
-f, -file stringDockerfile名称
-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.4 选择父镜像

大部分情况下,生成新的镜像都需要通过FROM指令来指定父镜像。父镜像是生成镜像的基础,会直接影响到所生成镜像的大小和功能。

用户可以选择两种镜像作为父镜像,一种是所谓的基础镜像(baseimage),另外一种是普通的镜像(往往由第三方创建,基于基础镜像)。

基础镜像比较特殊,其Dockerfile中往往不存在FROM指令,或者基于scratch镜像(FROM scratch)这意味着其在整个镜像树中处于根的位置。

普通镜像也可以作为父镜像来使用, 包括常见的busybox、debian、ubuntu等。

3.5 使用.dockerignore文件

可 以通过.dockerignore文件(每一行添加一条匹配模式)来让Docker忽略匹配路径或文件,在创建镜像时候不将无关数据发送到服务端。

# . dockerignore 文件中可以 定义忽略模式
*/temp*
*/*/temp*
tmp?
-*
Dockerfile
!README.md
  • dockerignore文件中模式语法支持Golang风格的路径正则格式:
  • "*"表示任意多个字符;
  • "?"代表单个字符;
  • "!"表示不匹配(即不忽略指定的路径或文件)。

3.6 其它docker build的用法

3.6.1 直接用Git repo进行构建

或许已经注意到了,docker build还支持从URL构建,比如可以直接从Git repo中构建:

# $env:DOCKER_BUILDKIT=0
# export DOCKER_BUILDKIT=0

$ docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world

Step 1/3 : FROM scratch
 --->
Step 2/3 : COPY hello /
 ---> ac779757d46e
Step 3/3 : CMD ["/hello"]
 ---> Running in d2a513a760ed
Removing intermediate container d2a513a760ed
 ---> 038ad4142d2b
Successfully built 038ad4142d2b

这行命令指定了构建所需的Git repo,并且指定分支为master,构建目录为/amd64/hello-world/,然后Docker就会自己去git clone这个项目、切换到指定分支、并进入到指定目录后开始构建。

3.6.2 用给定的tar压缩包构建

$ docker build http://server/context.tar.gz

如果所给出的URL不是个Git repo,而是个tar压缩包,那么Docker引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。

3.6.3 从标准输入中读取Dockerfile进行构建

docker build - < Dockerfile
或
cat Dockerfile | docker build -

如果标准输入传入的是文本文件,则将其视为Dockerfile,并开始构建。这种形式由于直接从标准输入中读取 Dockerfile的内容,它没有上下文,因此不可以像其他方法那样可以将本地文件 COPY 进镜像之类的事情。

3.6.4 从标准输入中读取上下文压缩包进行构建

$ docker build - < context.tar.gz

如果发现标准输入的文件格式是gzip、bzip2以及xz的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。

4 Dockerfile多阶段创建

4.1 之前的做法

在Docker 17.05版本之前,我们构建Docker镜像时,通常会采用两种方式:

4.1.1 全部放入一个Dockerfile

一种方式是将所有的构建过程编包含在一个Dockerfile中,包括项目及其依赖库的编译、测试、打包等流程,这里可能会带来的一些问题:

  • 镜像层次多,镜像体积较大,部署时间变长
  • 源代码存在泄露的风险

例如,编写app.go文件,该程序输出Hello World!

package main

import "fmt"

func main(){
    fmt.Printf("Hello World!");
}

编写 Dockerfile.one 文件

FROM golang:alpine

RUN apk --no-cache add git ca-certificates

WORKDIR /go/src/github.com/go/helloworld/

COPY app.go .

RUN go get -d -v github.com/go-sql-driver/mysql \
  && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . \
  && cp /go/src/github.com/go/helloworld/app /root

WORKDIR /root/

CMD ["./app"]

构建镜像

$ docker build -t go/helloworld:1 -f Dockerfile.one .

4.1.2 分散到多个Dockerfile

另一种方式,就是我们事先在一个Dockerfile将项目及其依赖库编译测试打包好后,再将其拷贝到运行环境中,这种方式需要我们编写两个Dockerfile和一些编译脚本才能将其两个阶段自动整合起来,这种方式虽然可以很好地规避第一种方式存在的风险,但明显部署过程较复杂。

例如,编写Dockerfile.build文件

FROM golang:alpine

RUN apk --no-cache add git

WORKDIR /go/src/github.com/go/helloworld

COPY app.go .

RUN go get -d -v github.com/go-sql-driver/mysql \
  && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

编写Dockerfile.copy文件

FROM alpine:latest

RUN apk --no-cache add ca-certificates

WORKDIR /root/

COPY app .

CMD ["./app"]

新建 build.sh

#!/bin/sh
echo Building go/helloworld:build

docker build -t go/helloworld:build . -f Dockerfile.build

docker create --name extract go/helloworld:build
docker cp extract:/go/src/github.com/go/helloworld/app ./app
docker rm -f extract

echo Building go/helloworld:2

docker build --no-cache -t go/helloworld:2 . -f Dockerfile.copy
rm ./app

现在运行脚本即可构建镜像

$ chmod +x build.sh

$ ./build.sh

对比两种方式生成的镜像大小

$ docker image ls

REPOSITORY      TAG    IMAGE ID        CREATED         SIZE
go/helloworld   2      f7cf3465432c    22 seconds ago  6.47MB
go/helloworld   1      f55d3e16affc    2 minutes ago   295MB

4.2 使用多阶段构建

为解决以上问题,自17.05版本开始,Docker支持多步骤镜像创建(Multi-stage build)特性,可以精简最终生成的镜像大小 。

对于需要编译的应用(如C、Go或Java语言等)来说,通常情况下至少需要准备两个环境的Docker镜像:

  • 编译环境镜像:包括完整的编译引擎、依赖库等,往往比较庞大。 作用是编译应用为二进制文件;
  • 运行环境镜像:利用编译好的二进制文件,运行应用,由于不需要编译环境,体积比较小。

使用多步骤创建,可以在保证最终生成的运行环境镜像保持精筒的情况下,使用单一的Dockerfile,降低维护复杂度。

例如,上述示例重新编写Dockerfile文件

FROM golang:alpine as builder

RUN apk --no-cache add git

WORKDIR /go/src/github.com/go/helloworld/

RUN go get -d -v github.com/go-sql-driver/mysql

COPY app.go .

RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest as prod

RUN apk --no-cache add ca-certificates

WORKDIR /root/

COPY --from=0 /go/src/github.com/go/helloworld/app .

CMD ["./app"]

构建镜像

$ docker build -t go/helloworld:3 .

对比三个镜像大小

$ docker image ls

REPOSITORY        TAG   IMAGE ID         CREATED            SIZE
go/helloworld     3     d6911ed9c846     7 seconds ago      6.47MB
go/helloworld     2     f7cf3465432c     22 seconds ago     6.47MB
go/helloworld     1     f55d3e16affc     2 minutes ago      295MB

很明显使用多阶段构建的镜像体积小,同时也完美解决了上边提到的问题。

4.3 只构建某一阶段的镜像

使用as来为某一阶段命名,例如

FROM golang:alpine as builder

当只想构建builder阶段的镜像时,增加--target=builder参数即可

$ docker build --target builder -t username/imagename:tag .

4.4 构建时从其他镜像复制文件

上面例子中使用COPY --from=0 /go/src/github.com/go/helloworld/app .从上一阶段的镜像中复制文件。

我们也可以复制任意镜像中的文件$ COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值