Dockerfile 是一个文本格式的配置文件, 我们可以使用 Dockerfile 来快速创建自定义的镜像。
Dockerfile由一行行命令语句组成,并且支持以#开头的注释行。
一般而言,Dockerfile分为四部分:
#第一行必须指定基于的基础镜像
FROM ubuntu:xeniel
#维护者信息
MAINTAINER docker_user [email protected]
#镜像的操作指令
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指令,来指定运行容器时的操作命令。
在debian:jessie
基础镜像基础上安装Nginx环境, 从而创建一个新的nginx镜像
FROM debian:jessie
LABEL maintainer docker_user
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;"]
基于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/
指令的一般格式为INSTRUCTION arguments
,指令包括"配置指令(配置镜像信息)“和"操作指令(具体执行操作)”。
分类 | 指令 | 说明 |
---|---|---|
配置指令 | ARG | 定义创建镜像过程中使用的变量 |
配置指令 | FROM | 指定所创建镜像的基础镜像 |
配置指令 | LABEL | 为生成的镜像添加元数据标签信息 |
配置指令 | EXPOSE | 声明镜像内服务监听的端口 |
配置指令 | ENV | 指定环境变量 |
配置指令 | ENTRYPOINT | 指定镜像的默认入口命令 |
配置指令 | VOLUME | 创建一个数据卷挂载点 |
配置指令 | USER | 指定运行容器时的用户名或UID |
配置指令 | WORKDIR | 配置工作目录 |
配置指令 | ONBUILD | 创建子镜像时指定自动执行的操作指令 |
配置指令 | STOPSIGNAL | 指定退出的信号值 |
配置指令 | HEALTHCHECK | 配置所启动容器如何进行健康检查 |
配置指令 | SHELL | 指定默认shell类型 |
操作指令 | RUN | 运行指定命令 |
操作指令 | CMD | 启动容器时指定默认执行的命令 |
操作指令 | ADD | 添加内容到镜像 |
操作指令 | COPY | 复制内容到镜像 |
定义创建镜像过程中使用的变量。格式为
ARG [=]
在执行docker build
时, 可以通过-build-arg[=]
来为变量赋值。
ARG
和ENV
的效果一样,都是设置环境变量。所不同的是,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_PROXY
、HTTPS_PROXY
、FTP_PROXY
、NO_PROXY
。
指定所创建镜像的基础镜像。格式为:
FROM [AS ]
或
FROM : [AS ]
或
FROM @ [AS ] 。
任何Dockerfile中第一条指令必须为FROM
指令。并且,如果在同一个Dockerfile中创建多个镜像时,可以使用多个FROM
指令(每个镜像一次)。
为了保证镜像精简,可以选用体积较小的镜像如Alpine或Debian作为基础镜像。 例如:
ARG VERSION=9.3
FROM debian:${VERSION}
在Docker Hub上有非常多的高质量的官方镜像:
nginx
、redis
、mongo
、mysql
、httpd
、php
、tomcat
等;node
、openjdk
、python
、ruby
、golang
等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如ubuntu
、debian
、centos
、fedora
、alpine
等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
除了选择现有镜像为基础镜像外,Docker还存在一个特殊的镜像,名为scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
FROM scratch
...
如果你以scratch
为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于Linux下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接FROM scratch
会让镜像体积更加小巧。使用Go语言开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为Go是特别适合容器微服务架构的语言的原因之一。
LABEL指令可以为生成的镜像添加元数据标签信息。 这些信息可以用来辅助过滤出特定镜像。格式为:
LABEL = = = ...
例如:
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
声明镜像内服务监听的端口。格式为:
EXPOSE [/... ]
例如:
EXPOSE 22 80 8443
注意该指令只是起到声明作用,并不会自动完成端口映射。如果要映射端口出来,在启动容器时可以使用-P
参数(Docker主机会自动分配一个宿主机的临时端口)或-p HOST_PORT:CONTAINER_PORT
参数(具体指定所映射的本地端口)。
指定环境变量,在镜像生成过程中会被后续RUN指令使用,在镜像启动的容器中也会存在。格式为:
ENV
或
ENV = =...
例如:
ENV APP VERSION=1.0.0
ENV APP_HOME=/usr/local/app
ENV PATH $PATH:/usr/local/bin
指令指定的环境变量在运行时可以被覆盖掉, 如
docker run --env = built_image
注意当一条ENV指令中同时为多个环境变量赋值并且值也是从环境变量读取时,会为变量都赋值后再更新。如下面的指令,最终结果为keyl=valuel key2=value2
:
ENV key1=value2
ENV key1=valuel key2=${key1}
下列指令可以支持环境变量展开:ADD
、COPY
、ENV
、EXPOSE
、FROM
、LABEL
、USER
、WORKDIR
、VOLUME
、STOPSIGNAL
、ONBUILD
、RUN
。
指定镜像的默认入口命令,该入口命令会在启动容器时作为根命令执行, 所有传人值作为该命令的参数。支持两种格式:
ENTRYPOINT ["executable", "param1", "param2"]
: exec调用执行;ENTRYPOINT command param1 param2
: shell中执行。ENTRYPOINT
的目的和CMD
一样,都是在指定容器启动程序及参数。ENTRYPOINT
在运行时也可以替代,不过比CMD
要略显繁琐,需要通过docker run
的参数--entrypoint
来指定。
当指定了ENTRYPOINT
后,CMD
的含义就发生了改变,不再是直接的运行其命令,而是将CMD
的内容作为参数传给ENTRYPOINT
指令,换句话说实际执行时,将变为:
""
那么有了CMD
后,为什么还要有ENTRYPOINT
呢?这种
有什么好处么?让我们来看几个场景。
假设我们需要一个得知自己当前公网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
中去执行,而这个脚本会将接到的参数(也就是
)作为命令,在脚本最后执行。比如官方镜像 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用户,并在最后指定了ENTRYPOINT
为docker-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
, 当指定多个时,只有最后一个起效。
创建一个数据卷挂载点。格式为:
VOLUME ["<路径1>", "<路径2>"...]
或
VOLUME <路径>
运行容器时可以从本地主机或其他容器挂载数据卷,一般用来存放数据库和需要保持的数据等。
容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在Dockerfile中,可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。
VOLUME /data
这里的/data
目录就会在容器运行时自动挂载为匿名卷,任何向/data
中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行容器时可以覆盖这个挂载设置。比如:
$ docker run -d -v mydata:/data xxxx
在这行命令中,就使用了mydata
这个命名卷挂载到了/data
这个位置,替代了Dockerfile中定义的匿名卷的挂载配置。
指定运行容器时的用户名或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" ]
为后续的RUN
,CMD
,ENTRYPOINT
指令配置工作目录,以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR
会帮你建立目录。格式为:
WORKDIR path /to/workdir
可以使用多个WORKDIR
令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
则最终路径为/a/b/c
。因此,为了避免出错,推荐WORKDIR
指令中只使用绝对路径。
指定当基于所生成镜像创建子镜像时,自动执行的操作指令。格式为
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指令在创建专门用于自动编译、检查等操作的基础镜像时,十分有用。
指定所创建镜像启动的容器接收退出的信号值
STOPSIGNAL signal
配置所启动容器如何进行健康检查(如何判断健康与否),自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": "\n\n\nWelcome to nginx! \n\n\n\nWelcome to nginx!
\nIf you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.
\n\nFor online documentation and support please refer to\nnginx.org.
\nCommercial support is available at\nnginx.com.
\n\nThank you for using nginx.
\n\n\n",
"Start": "2016-11-25T14:35:37.780192565Z"
}
],
"Status": "healthy"
}
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
运行指定命令。格式为:
RUN <命令>
默认将在shell终端中运行命令,即/bin/sh -c
;RUN
# 例如
RUN echo 'Hello, Docker!
' > /usr/share/nginx/html/index.html
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/*
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制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。
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
指定的命令。
添加内容到镜像。格式为:
ADD
该命令将复制指定的
路径下内容到容器中的
路径下。其中
可以是Dockerfile所在目录的一个相对路径(文件或目录);也可以是一个URL;还可以是一个tar文件(自动解压为目录)
可以是镜像内绝对路径,或者相对于工作目录(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=
选项来改变文件的所属用户及所属组。
ADD --chown=55:mygroup files* /mydir/
ADD --chown=bin files* /mydir/
ADD --chown=1 files* /mydir/
ADD --chown=10:11 files* /mydir/
另外需要注意的是,ADD
指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。
复制内容到镜像。和RUN
指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。格式为:
COPY [--chown=:] ...
COPY [--chown=:] ["",... ""]
COPY
指令将从构建上下文目录中
的文件/目录复制到新的一层的镜像内的
位置。目标路径不存在时,会自动创建。例如:
COPY package.json /usr/src/app/
可以是多个,甚至可以是通配符,其通配符规则要满足Go的filepath.Match
规则,如:
COPY hom* /mydir/
COPY hom?.txt /mydir/
可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用WORKDIR
指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
此外,还需要注意一点,使用COPY
指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用Git进行管理的时候。
在使用该指令的时候还可以加上--chown=
选项来改变文件的所属用户及所属组。
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
的场合,就是所提及的需要自动解压缩的场合。
编写完成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/
Dockerfile为:
(base) pang@pang-HP:~/dockerfile$ pwd
/home/pang/dockerfile
(base) pang@pang-HP:~/dockerfile$ cat Dockerfile
FROM nginx:alpine
RUN echo 'Hello, Docker!
' > /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 'Hello, Docker!
' > /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$
docker build
命令最后有一个.
。 .
表示当前目录,而Dockerfile就在当前目录,而这个路径不能单纯的理解为是Dockerfile的路径,而是上下文路径。
首先根据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引擎
无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。
现在就可以理解刚才的命令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引擎
的。
在默认情况下,如果不额外指定Dockerfile的话,会将上下文目录下的名为Dockerfile的文件作为Dockerfile。这只是默认行为,实际上Dockerfile的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如可以用-f ../Dockerfile.php
参数指定某个文件作为 Dockerfile。当然,一般大家习惯性的会使用默认的文件名 Dockerfile,以及会将其置于镜像构建上下文目录中。
另外,要指定生成镜像的标签信息,可以通过-t
选项 该选项可以重复使用多次为镜像一次添加多个名称。
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 int |
CFS调度都总份额 |
-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的配置 |
大部分情况下,生成新的镜像都需要通过FROM
指令来指定父镜像。父镜像是生成镜像的基础,会直接影响到所生成镜像的大小和功能。
用户可以选择两种镜像作为父镜像,一种是所谓的基础镜像(baseimage),另外一种是普通的镜像(往往由第三方创建,基于基础镜像)。
基础镜像比较特殊,其Dockerfile中往往不存在FROM
指令,或者基于scratch镜像(FROM scratch)这意味着其在整个镜像树中处于根的位置。
普通镜像也可以作为父镜像来使用, 包括常见的busybox、debian、ubuntu等。
可 以通过.dockerignore
文件(每一行添加一条匹配模式)来让Docker忽略匹配路径或文件,在创建镜像时候不将无关数据发送到服务端。
# . dockerignore 文件中可以 定义忽略模式
*/temp*
*/*/temp*
tmp?
-*
Dockerfile
!README.md
dockerignore
文件中模式语法支持Golang风格的路径正则格式:或许已经注意到了,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这个项目、切换到指定分支、并进入到指定目录后开始构建。
$ docker build http://server/context.tar.gz
如果所给出的URL不是个Git repo,而是个tar压缩包,那么Docker引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。
docker build - < Dockerfile
或
cat Dockerfile | docker build -
如果标准输入传入的是文本文件,则将其视为Dockerfile,并开始构建。这种形式由于直接从标准输入中读取 Dockerfile的内容,它没有上下文,因此不可以像其他方法那样可以将本地文件 COPY 进镜像之类的事情。
$ docker build - < context.tar.gz
如果发现标准输入的文件格式是gzip、bzip2以及xz的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。
在Docker 17.05版本之前,我们构建Docker镜像时,通常会采用两种方式:
一种方式是将所有的构建过程编包含在一个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 .
另一种方式,就是我们事先在一个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
为解决以上问题,自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
很明显使用多阶段构建的镜像体积小,同时也完美解决了上边提到的问题。
使用as
来为某一阶段命名,例如
FROM golang:alpine as builder
当只想构建builder阶段的镜像时,增加--target=builder
参数即可
$ docker build --target builder -t username/imagename:tag .
上面例子中使用COPY --from=0 /go/src/github.com/go/helloworld/app .
从上一阶段的镜像中复制文件。
我们也可以复制任意镜像中的文件$ COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
。