Docker之 Dockerfile 使用教程

0x01 引言

实际环境中,项目所需要的容器可能没有很符合的公共镜像,就需要在公共镜像的基础上重新构建镜像,除了可以从容器中用 docker commit 命令创建,还可以使用 Dockerfile 文件来直接定制镜像

0x02 简介

Dockerfile 是用来构建 Docker 镜像的构建文件,它是由一系列命令和参数构成的脚本,Docker 通过读取 Dockerfile 中的指令自动生成映像

Dockerfile、Docker 镜像、Docker 容器的区别

Dockerfile Docker 镜像 Docker 容器
软件的原材料 软件的交付品 软件的运行态
面向开发 交付标准 部署与运维
定义进程需要的一切东西 根据 Dockerfile 定义构建镜像 容器是直接提供服务的

构建镜像

使用 docker build 命令会根据 Dockerfile 文件及上下文构建新 Docker 镜像。构建上下文是指 Dockerfile 所在的本地路径或Git仓库地址。构建上下文环境会被递归处理,所以构建所指定的路径还包括了子目录,而 URL 还包括了其中指定的子模块

保留字

保留字及其代表含义

保留字 注释
[FROM 镜像:版本标签] 当前新镜像是基于哪个镜像
[LABEL key=value] 也叫 MAINTAINER 脚本说明,打标签
[RUN 命令] 容器构建时需要运行的命令
[EXPOSE 端口] 当前镜像对外暴露的端口,在 docker run -p 的时候生效
[EVN key] [value] 设置容器的环境变量,能够被文件调用
[WORKDIR 目录] 指定工作目录
ADD “源文件” “目标路径” 将宿主机目录下的文件拷贝进镜像,如果文件是 tar 压缩文件会自动解压,如果文件是 url 则不会解压
COPY “源文件” “目标路径” 和 ADD 类似,只是少了解压功能
[VOLUME 目录] 在主机上创建一个挂载,挂载到容器的指定路径,用于数据保存和持久化工作
[CMD 命令] 容器启动时要运行的命令,存在有多个 CMD 指令时只有最后一个生效,会被 docker run 之后的参数替换
ENTRYPOINT 容器启动时运行的命令,一定会执行
ONBUILD 当构建一个被继承的 Dockerfile 时运行命令,父镜像在被子继承后父镜像的 ONBUILD 被触发

注意:编写时能不加引号就不要加

FROM 指定基础镜像

定制的镜像基本基于公共镜像的基础上,后续的操作就是在这个镜像的基础上进行修改


Dockerfile 的第一条指令必须为 FROM 指令,可以使用多个 FROM 指令

格式

1
2
3
FROM <image>
FROM <image>:<tag>
FROM <image>@<digest>

第一行必须指定基础容器,这里的是 tomcat8

1
FROM tomcat:8

MAINTAINER 指定维护者信息

格式

1
MAINTAINER <name>

注意:MAINTAINER 指令已经被抛弃,建议使用 LABEL 指令

1
MAINTAINER hello <admin@2ba.cc>

LABEL 为镜像添加元数据

为镜像添加标签,一个 LABEL 就是一个键值对,也可以一行指定多个键值对

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

可以用一些标签来申明镜像的作者、版本等

1
2
3
4
5
6
7
# 多行指定信息
LABEL com.example.label-with-value="foo
LABEL version="1.0"
LABEL description="这是简介信息"

# 一行指定多个键值对
LABEL app.version="1.0" app.host='2ba.cc' description="这是简介信息"

如果新添加的 LABEL 和已有的 LABEL 同名,则新值会覆盖掉旧值

RUN

每条 RUN 指令将在当前镜像的基础上执行指定命令,并提交为新的镜像

多个操作建议使用 && 连接,这样能够减少镜像分层,每个 RUN 一层 \ 可以换行

一种直接接Shell命令

1
2
RUN <command>
RUN apk update

一种接可执行文件,可包含参数传递

1
2
RUN ["executable", "参数1", "参数2"]
RUN ["/etc/execfile", "arg1", "arg1"]

Dockerfile 的指令每执行一次都会在 Docker 上新建一层,所以过多的命令就会有过多的无意义的层,容易造成镜像膨胀过大,在写法上可以进行优化,如下这样以 && 符号链接命令,这样执行后,只会创建一层镜像

1
2
3
4
FROM centos
RUN yum install wget -y
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN tar -xvf redis.tar.gz

以上执行会创建 3 层镜像

可简化为以下格式

1
2
3
4
FROM centos
RUN yum install wget -y \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& tar -xvf redis.tar.gz

CMD 容器启动命令

指定启动容器时执行的命令,类似于 RUN 指令,但二者运行的时间点不同

  • CMDdocker run 容器启动后运行
  • RUNdocker build 构建镜像时执行

每个 Dockerfile 只能有一条 CMD 命令,多个 CMD 也只会执行最后一个

如果 docker run 的时候后面有命令,如 /bin/sh,则 CMD 不会执行

1
2
3
CMD <命令>                             # shell 格式
CMD ["可执行文件", "参数1", "参数2"...] # exec 格式
CMD ["参数1", "参数2"...] # 参数列表格式

示例

1
2
CMD echo "This is a test." | wc -
CMD ["/usr/bin/wc","--help"]

COPY 复制文件

复制指令,从上下文目录中复制文件或目录 到容器里指定路径

1
COPY [--chown=<user>:<group>] <源路径> <目标路径>
  • [--chown=]:改变容器内文件的拥有者和属组
  • <源路径>:源文件或者源目录,可以是多个,支持 通配符
  • <目标路径>:指定容器内路径,不存在的话会自动创建

ADD 更高级的复制文件

使用格式跟 COPY 一致(同样需求下,官方推荐使用 COPY)

该命令将复制指定的 <src> 到容器中的 <dest>

1
2
ADD <src>... <dest>
ADD ["<src>",... "<dest>"] # 用于支持包含空格的路径

其中 <src> 可以是 Dockerfile 所在目录的一个相对路径的文件或目录;也可以是一个 URL,还可以是一个 tar文件,tar 类型文件会自动解压(网络压缩资源不会被解压),可以访问网络资源,类似 wget

示例

1
2
3
4
ADD hom* /mydir/        # 添加所有以"hom"开头的文件
ADD hom?.txt /mydir/ # ? 替代一个单字符,例如:"home.txt"
ADD test relativeDir/ # 添加 "test" 到 `WORKDIR`/relativeDir/
ADD test /absoluteDir/ # 添加 "test" 到 /absoluteDir/

ENV 设置环境变量

定义了环境变量,那么在后续的 RUN 指令中,就可以使用这个环境变量

格式

1
2
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN 还是运行时的应用,都可以直接使用这里定义的环境变量

1
2
3
ENV JAVA_HOME /usr/java/jdk1.8.0_202-amd64
ENV myName John Doe
ENV myCat=fluffy

ENTRYPOINT

配置容器启动后执行的命令,一定会执行,并且不可被 docker run 提供的参数覆盖

每个 Dockerfile 中只能有一个 ENTRYPOINT,当指定多个 ENTRYPOINT 时,只有最后一个生效

1
2
ENTRYPOINT ["executable", "参数1", "参数2"]
ENTRYPOINT command param1 param2 (shell 中执行)

VOLUME 定义匿名卷

用于指定持久化目录

一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能

  1. 卷可以容器间共享和重用
  2. 容器并不一定要和其它容器共享卷
  3. 修改卷后会立即生效
  4. 对卷的修改不会对镜像产生影响
  5. 卷会一直存在,直到没有任何容器在使用它

格式

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

示例

1
2
VOLUME "/data"
VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"]

WORKDIR 指定工作目录

指定的工作目录,会在构建镜像的每一层中都存在,WORKDIR 指定的工作目录,必须是提前创建好的

1
WORKDIR <工作目录路径>

示例

1
2
3
WORKDIR /a  # 这时工作目录为 /a
WORKDIR b # 这时工作目录为 /a/b
WORKDIR c # 这时工作目录为 /a/b/c

EXPOSE 声明端口

指定于外界交互的端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务

在启动容器时需要通过 -P 参数让 Docker 主机分配一个端口转发到指定的端口。使用 -p 参数则可以具体指定主机上哪个端口映射过来

格式

1
EXPOSE <端口1> [<端口2>...]

示例

1
2
3
EXPOSE 80 443
EXPOSE 8080
EXPOSE 11211/tcp 11211/udp

注:EXPOSE并不会让容器的端口访问到主机。要使其可访问,需要在 docker run 运行容器时通过-p来发布这些端口,或通过 -P 参数来发布 EXPOSE 导出的所有端口

USER 指定当前用户

指定运行容器时的用户名或 UID,后续的 RUN 也会使用指定用户。当服务不需要管理员权限时,可以通过该命令指定运行用户。用户和用户组必须提前已经存在

格式

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

示例

1
2
3
USER daemon
# 或
RUN groupadd -r postgres && useradd -r -g postgres postgres

0x03 例子

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

一般而言,Dockerfile 的内容分为四个部分

  • 基础镜像信息
  • 维护者信息
  • 镜像操作指令
  • 容器启动时执行指令

Dockerfile 完整 demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
cat<<'EOF'>dockerfile
# 第一行必须指定基础镜像信息,建议使用aipln类型的小容器
FROM tomcat:8

# 维护者信息 (可选)
MAINTAINER hello admin@2ba.cc

# 标签信息 (可选,自定义信息,多标签可放一行)
LABEL app.maintainer=demo
LABEL app.version="1.0" app.host='2ba.cc' description="这是个Domo"

# 环境变量(可选,指定一个环境变量,会被后续 RUN 指令使用,并在容器运行时保持)
# ENV MYPATH /usr/local
ENV JAVA_HOME /opt/java_jdk/bin
ENV PG_VERSION 9.3.4
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH

# 配置 java 与 tomcat 环境变量
# ENV JAVA_HOME /usr/local/jdk1.8.0_261
# ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
# ENV CATALINA_HOME $MYPATH/apache-tomcat-8.5.60
# ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin

# 指定容器工作目录,后续的 RUN、CMD、ENTRYPOINT 指令配置容器内的工作目录
WORKDIR /usr

# 可选,指定运行容器时的用户名或 UID,后续的 RUN 也会使用指定用户,前面的 RUN 不受影响
# RUN groupadd -r postgres && useradd -r -g postgres postgres
USER postgres

# 设置工作访问时候的 WORKDIR 路径
# 后续的 RUN、CMD、ENTRYPOINT 指令配置容器内的工作目录
# WORKDIR $MYPATH/apache-tomcat-8.5.60/
WORKDIR /path/to/workdir

# ADD/COPY 将外部文件copy到容器中
# 区别是ADD可以使用URL,还可以是tar,会自动解压
# COPY只能使用dockerfile所在目录
# ADD <src> <dest>
# COPY <src> <dest>
COPY target/tomcat-release.war /usr/local/tomcat/webapps/

# 把 java 与 tomcat 添加到容器中指定目录下
# ADD jdk-8u261-linux-x64.tar.gz $MYPATH/
# ADD apache-tomcat-8.5.60.tar.gz $MYPATH/

# RUN 镜像的操作指令
# RUN <command> ["executable", "param1", "param2"]
RUN echo "deb http://archive.ubuntu.com/ubuntu/ raring main universe" >> /etc/apt/sources.list \
&& RUN apt-get update \
&& apt-get install -y nginx && mkdir /opt/deploy/ \
&& RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf

# EXPOSE 容器启动后需要暴露的端口
EXPOSE 22 80 8443 8080

# VOLUME 本地或其他容器挂载的挂载点,一般用来存放数据库和需要保持的数据等
# VOLUME ["/data"]
# VOLUME $CATALINA_HOME/webapps/
VOLUME ["/data/postgres", "/other/path/"]

# ENTRYPOINT 容器启动后执行命令,不会被 docker run 提供的参数覆盖,只能有一个ENTRYPOINT
# 多个ENTRYPOINT,以最后一个为准
# ENTRYPOINT ["executable", "param1", "param2"]
# ENTRYPOINT command param param2
ENTRYPOINT echo "helloDocker"

# 容器启动时执行命令,每个 Dockerfile 只能有一条 CMD 命令
# CMD ["executable", "param1", "param2"] 使用 exec 执行,推荐方式
# CMD command param1 param2 在 /bin/sh 中执行,提供给需要交互的应用
# CMD ["param1", "param2"] 提供给 ENTRYPOINT 的默认参数
# 启动时运行 tomcat 并打印日志
# CMD $CATALINA_HOME/bin/startup.sh && tail -F $CATALINA_HOME/logs/catalina.out
CMD /usr/sbin/nginx

EOF

构建镜像

1
docker build -f ./tomcat_dockerfile -t mytomcat:7 .
选项 注释
-f 构建文件路径
-t 镜像名称:版本号
. 当前路径

启动镜像

1
docker run -it --name ok -p 9080:8080 mytomcat:7