Docker 镜像构建
Dockerfile
以一个镜像为基础,通过指令构建其他镜像。
构建Docker镜像
在Dockerfile所在目录执行docker build -t 镜像名:标签 .(上下文路径)
创建新的镜像。
默认Docker在构建的时候会从上下文路径中查找名字为Dockerfile
的文件,来执行构建。
Dockerfile中所有针对宿主机文件的指令均以上下文路径作为工作目录。
可以使用-f 指定Dockerfile的路径和context路径,例如
docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context
使用标准输入(STDIN)方式传入Dockerfile和context的内容并构建镜像
docker build -t myimage:latest -<<EOF
FROM busybox
RUN echo "hello world"
EOF
使用当前目录作为context,标准输入(STDIN)方式传入Dockerfile构建镜像
docker build -t myimage:latest -f- . <<EOF
FROM busybox
COPY somefile.txt .
RUN cat /somefile.txt
EOF
使用远程的context,标准输入(STDIN)方式传入Dockerfile构建镜像
docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <<EOF
FROM busybox
COPY hello.c .
EOF
.dockerignore 文件
和.gitignore
文件作用类似,.dockerignore
文件用来排除build context中的特定文件或目录。
.dockerignore
文件的语法和.gitignore
文件相同。
Dockerfile 指令
FROM
Dockerfile的第一个指令。用于指定镜像从什么基础镜像开始构建。
常用的基础镜像有:
- busybox:一个集成了常用Linux命令的基础工具箱。
- alpine:推荐使用,具备完整的Linux功能,体积非常小,只有5M左右,使用apk工具安装软件包。
- centos:CentOS系统。
- ubuntu:Ubuntu系统。
- jdk:自带指定版本JDK的基础镜像。可以作为Java项目的基础镜像。
- maven:自带maven的基础镜像。可以作为编译环境构建的基础镜像。
- scratch:没有任何基础镜像,从零开始构建。
LABEL
LABEL和注释类似,负责给镜像添加一些额外的key-value信息。
使用例子:
# Set one or more individual labels
LABEL com.example.version="0.0.1-beta"
LABEL vendor1="ACME Incorporated"
LABEL vendor2=ZENITH\ Incorporated
LABEL com.example.release-date="2015-02-12"
LABEL com.example.version.is-production=""
注意:字符串的value如果带有空格,必须要用引号引起来。字符串内部的引号同样需要引号引起来。
Docker1.10之前不推荐使用多个LABEL,因为每一个LABEL指令会生成一个新的镜像层。但是Docker1.10之后可以不用刻意这么做。
# Set multiple labels on one line
LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"
LABEL一行指令还可以分到多行来写,例如:
# Set multiple labels at once, using line-continuation characters to break long lines
LABEL vendor=ACME\ Incorporated \
com.example.is-beta= \
com.example.is-production="" \
com.example.version="0.0.1-beta" \
com.example.release-date="2015-02-12"
RUN
构建镜像时执行Linux命令。RUN执行使用sh -c
来运行命令。
注意:
- RUN执行每执行一次会产生一个新的镜像层,尽量不要产生过多的镜像层。
- 构建镜像每一层都会生成一个缓存。例如:
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl
运行apt-get update之后的结果会生成一个缓存。
如果之后修改了这个Dockerfile,增加一个安装nginx包,Docker仍会使用之前apt-get update之后的缓存继续构建。这样会存在问题。新构建的镜像使用的包不是构建时最新的。要解决这个问题,需要将update和安装命令写到同一行。
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo=1.3.*
- 使用命令pipeline,只要最后一个命令运行正常,这条RUN命令会被认为执行成功。例如:
RUN wget -O - https://some.site | wc -l > /number
如果wget运行失败,这条RUN执行仍会执行成功。
如果必须要求pipeline前后命令都执行成功,可以使用如下命令:
RUN set -o pipefail && wget -O - https://some.site | wc -l > /number
如果使用的是Debian系Linux镜像,默认使用的dash不支持set -o pipefail
。需要明确指定bash去执行命令,如下所示:
RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]
CMD
用于指定运行镜像中用户程序的入口。可以带有启动参数。
CMD指令的使用形式如下:
CMD ["executable", "param1", "param2"…]
注意:用户进程绝对不能以后台形式或者服务形式运行,否则container会认为主线程执行完毕退出。必须使用前台形式运行用户进程。
Docker运行时候用户可以指定入口命令。CMD中的入口命令会被覆盖。
一个例子如下
CMD ["/bin/bash", "-c", "Hello World"]
默认启动container,会打印出"Hello World"
如果启动container的时候使用参数echo "Blabla"
,CMD会被覆盖,结果打印出"Blabla"。
ENTRYPOINT
ENTRYPOINT指令和CMD指令基本相同。使用形式如下:
ENTRYPOINT ["/bin/bash", "-c", "Hello World"]
但是ENTRYPOINT和CMD命令还有区别。如果用户在启动container时候指定了自定义命令,只会追加到ENTRYPOINT的命令之后,不会完整替换掉ENTRYPOINT命令。
例如:
ENTRYPOINT ["echo"]
启动container传入自定义命令Hello World
,container启动的时候会执行echo Hello World
打印出"Hello World"字样。
ENTRYPOINT通常和CMD配合使用。ENTRYPOINT指定用户程序入口命令,CMD指定命令的默认执行参数。例如:
ENTRYPOINT ["echo"]
CMD ["Hello World"]
默认运行会执行
echo Hello World
如果运行时指定了自定义命令,例如docker run -it example-image Hola
,此时"Hola"会替换掉CMD中的"Hello World",然后追加到ENTRYPOINT之后,相当于执行了:
echo Hola
编写入口脚本的时候最好使用exec命令执行用户程序,这样用户程序在container中的PID为1,方便接收到Unix信号。
EXPOSE
用于声明需要暴露端口,大多数情况用于增加可读性。
使用方式如下:
EXPOSE 8080
如果运行的时候使用了-P
参数,Docker会映射镜像所有EXPOSE的端口到宿主机任意端口。
docker run -it example-image -P
如果使用手工指定宿主机和容器的端口映射,EXPOSE将不起作用。
docker run -it example-image -p 80:8080
ENV
指定运行时container中的环境变量。
ENV设置的变量既可以在Dockerfile中使用,也可以在container内使用。
使用例子如下:
ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH
注意:ENV也会创建一个新的镜像层。这样会导致ENV设置的环境变量,无法被后续的RUN命令清除掉。
例如:
FROM alpine
ENV ADMIN_USER="mark"
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USER
尽管执行了unset,ADMIN_USER环境变量依然存在。
为了避免这种情况,需要把设置环境变量和使用它的地方写到同一层镜像的构建中。例如:
FROM alpine
RUN export ADMIN_USER="mark" \
&& echo $ADMIN_USER > ./mark \
&& unset ADMIN_USER
CMD sh
COPY
COPY用于构建时从宿主机向镜像中复制文件。
如果需要复制多组文件,尽量使用多个COPY指令,这样每次Docker缓存中只有一组文件的复制操作。还有COPY操作尽量在RUN指令之后,减少以后修改Dockerfile文件Docker缓存失效的影响返回,加快构建速度。
ADD
和COPY作用一样也是用于向镜像中添加文件。ADD还有额外的功能,比如说添加一个tar压缩包时会自动解压。ADD后面跟URL会自动从那里下载文件。但是,仅仅推荐在需要解压tar压缩包到镜像的时候才推荐使用。
使用方式:
ADD rootfs.tar.xz /
强烈建议不要用ADD执行下载URL指向的文件,应使用curl或者wget命令。
VOLUME
VOLUME指令用来标识需要挂载数据卷的目录。
使用方式如下:
VOLUME ["/data"]
运行这个镜像的时候会创建出来一个volume,挂载到/data
目录。
可以在运行命令中指定-v
参数覆盖,比如说将/data
目录映射到宿主机某个目录。
USER
默认RUN指令用root用户执行命令。如果需要切换到其他用户来执行,使用USER指令。
USER指令的使用方法为:
USER <user>[:<group>] or
USER <UID>[:<GID>]
注意:
- 在切换用户前需要先创建用户。例如执行:
RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres
- 因为Go语言的一个bug,在container内创建一个UID比较大的用户会导致container包含
/var/log/faillog
的一层填满NULL(\0),磁盘会耗尽。规避这个问题的方法是创建用户的时候useradd
命令加上--no-log-init
参数。Debian/Ubuntu不支持这个参数。 - 不要在构建镜像时使用
sudo
,应当使用gosu
。gosu
的安装和使用方式在https://github.com/tianon/gosu。 - 使用USER切换用户也会创建一个新镜像层,避免过多使用USER来回切换用户。
WORKDIR
切换当前工作目录。例如:
WORKDIR /data
RUN ls config-*
相当于
RUN ls /data/config-*
ARG
和ENV使用方法类似,但是ARG设定的变量只能在Dockerfile中访问,无法在container内部使用。
使用方法:
ARG <name>[=<default value>]
在构建镜像(docker build)的时候使用--build-arg <varname>=<value>
形式来给ARG赋值,或者说覆盖Dockerfile中ARG的默认值。
ONBUILD
ONBUILD指令在构建当前镜像的时候不会执行。但是,在其他镜像基于这个镜像构建(即 FROM 这个镜像)的时候,ONBUILD会执行。
ONBUILD的使用例子:
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
注意:专用于ONBUILD的镜像,最好给一个以onbuild结尾的tag,例如ruby:1.9-onbuild
。
MAINTAINER
注明镜像的维护者信息。
使用方法:
MAINTAINER <name>
注意:该指令已经废弃,建议使用label来替代。例子如下所示:
LABEL maintainer="SvenDowideit@home.org.au"