Dockerfile 是一个用来构建镜像的文本文件,包含了一条条构建镜像所需的指令和说明。
- Dockerfile 是一个文本文件
- 包含了一条条的指令
- 每一条指令构建一层,基于基础镜像,最终构建出一个新的镜像
- 对于开发人员:可以为开发团队提供一个完全一致的开发环境
- 对于测试人员:可以直接拿开发时所构建的镜像或者通过Dockerfile文件 构建一个新的镜像开始工作了
- 对于运维人员:在部署时,可以实现应用的无缝移植
常见的镜像在DockerHub就能找到,但是我们自己写的项目就必须自己构建镜像了。
dockerFile是用来构建docker镜像的文件!命令参数脚本!
构建自定义的镜像时,并不需要一个个文件去拷贝,打包。
我们只需要告诉Docker,我们的镜像的组成,需要哪些BaseImage、需要拷贝什么文件、需要安装什么依赖、启动脚本是什么,将来Docker会帮助我们构建镜像。
而要自定义镜像,就必须先了解镜像的结构才行。
镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。
我们以MySQL为例,来看看镜像的组成结构:
简单来说,镜像就是在系统函数库、运行环境基础上,添加应用程序文件、配置文件、依赖文件等组合,然后编写好启动脚本打包在一起形成的文件。
我们要构建镜像,其实就是实现上述打包的过程。
Dockerfile就是一个文本文件,其中包含一个个的指令(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer。
网上找到一张很有意思的图,我将它放在这。
基础镜像,定制的镜像都是基于 FROM
的镜像。并且必须是第一条指令。
如果不以任何镜像为基础,那么写法为:FROM scratch
。
FROM <image>
FROM <image>:<tag>
FROM <image>@<digest>
# 注意:
tag或digest是可选的,如果不使用这两个值时,会使用latest版本的基础镜像
FROM mysql:5.7
# 若不以任何镜像为基础
FROM scratch
镜像是谁写的, 姓名+邮箱。可以只写一个。
新版Docker中将弃用,并使用LABEL
指明。
MAINTAINER <name>
# 只写名字
MAINTAINER tyt
# 只写邮箱
MAINTAINER 273@qq.com
# 名字 + 邮箱
MAINTAINER tyt<273@qq.com>
LABEL
指令用来给镜像添加一些元数据(metadata),以键值对的形式。通俗点说就是为镜像指定标签。
LABEL <key>=<value> <key>=<value> <key>=<value> ...
LABEL org.opencontainers.image.authors="tyt" version="1.0" label1="l1"
镜像构建的时候需要运行的命令。有以下2种格式:
RUN <命令行命令>
# <命令行命令> 等同于,在终端操作的 shell 命令。
RUN ["可执行文件或命令", "参数1", "参数2", ...]
# RUN ["./test.php", "dev", "offline"] 等价于 RUN ./test.php dev offline
RUN yum -y install vim
RUN yum -y install net-tools
RUN ["/etc/execfile", "arg1", "arg1"]
# 以上执行会创建 3 层镜像。可简化为以下格式:
RUN yum -y install vim \
&& RUN yum -y install net-tools \
&& RUN ["/etc/execfile", "arg1", "arg1"]
# 如上,以 && 符号连接命令,这样执行后,只会创建 1 层镜像。
注意
RUN
指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定--no-cache
参数,如:docker build --no-cache
类似于 RUN
指令,用于运行程序,但二者运行的时间点不同:
CMD
在docker run
时运行。RUN
在docker build
时运行。
作用
- 为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束。
CMD
指令指定的程序可被docker run
命令行参数中指定要运行的程序所覆盖。注意
- 如果 Dockerfile 中如果存在多个
CMD
指令,仅最后一个生效。
# 格式1:执行可执行文件或命令
CMD ["<可执行文件或命令>","" ,"" ,...]
# 格式2:执行 shell 内部命令
CMD <shell命令>
# 格式3:该写法是为 ENTRYPOINT 指令指定的程序提供默认参数
# 注意,待会下面会提到
CMD ["" ,"" ,...]
推荐使用第一种格式,执行过程比较明确。
第二种格式实际上在运行的过程中也会自动转换成第一种格式运行,
并且默认可执行文件是 sh, 即 /bin/bash
例如
CMD ls -a
会转换为
CMD ["ls", "-a"]
# 执行可执行文件或命令
CMD ["/usr/bin/wc","--help"]
# 执行 shell 内部命令
CMD echo "This is a test." | wc -l
# 为 ENTRYPOINT 指令指定的程序提供默认参数
ENTRYPOINT ["nginx", "-c"]
CMD ["/etc/nginx/nginx.conf"]
默认执行时,运行以下命令
nginx -c /etc/nginx/nginx.conf
类似于 CMD
指令,运行时机相同,都是在docker run
执行,但其不会被 docker run
的命令行参数指定的指令所覆盖,而且这些命令行参数会被当作参数追加到 ENTRYPOINT
指令指定的程序。(具体谁被覆盖、谁被追加可看下方镜像构建实战中的 验证CMD 和ENTRYPOINT区别)。
CMD与ENTRYPOINT不同点
ENTRYPOINT
不会被运行的command覆盖,而CMD
则会被覆盖。但是, 如果运行docker run
时使用了--entrypoint
选项,将覆盖ENTRYPOINT
指令指定的程序。ENTRYPOINT优点
- 在执行
docker run
的时候可以指定ENTRYPOINT
运行所需的参数。注意
- 如果 Dockerfile 中如果存在多个
ENTRYPOINT
指令,仅最后一个生效。- 如果我们在Dockerfile种同时写了
ENTRYPOINT
和CMD
,并且CMD
指令不是一个完整的可执行命令,那么CMD
指定的内容将会作为ENTRYPOINT
的参数- 如果我们在Dockerfile种同时写了
ENTRYPOINT
和CMD
,并且CMD
是一个完整的指令,那么它们两个会互相覆盖,谁在最后谁生效
# 执行可执行文件
ENTRYPOINT ["<可执行文件或命令>","" ,"" ,...]
# 执行 shell 内部命令
ENTRYPOINT <shell命令>
假设已通过 Dockerfile 构建了 nginx:test 镜像:
FROM nginx
ENTRYPOINT ["nginx", "-c"] # 定参
CMD ["/etc/nginx/nginx.conf"] # 变参
-----------------------------------------------
不传参运行
docker run nginx:test
容器内会默认运行以下命令,启动主进程
nginx -c /etc/nginx/nginx.conf
-----------------------------------------------
传参运行
docker run nginx:test -c /etc/nginx/my.conf
容器内会默认运行以下命令,启动主进程(/etc/nginx/my.conf:假设容器内已有此文件)
nginx -c /etc/nginx/my.conf
# CMD指令不是一个完整的可执行命令
ENTRYPOINT ["nginx", "-c"]
CMD ["/etc/nginx/nginx.conf"]
# 那么CMD指定的内容将会作为ENTRYPOINT的参数
# 容器内会默认运行以下命令
nginx -c /etc/nginx/nginx.conf
------------------------------------------------
# CMD指令是一个完整的可执行命令
ENTRYPOINT ["nginx", "-c"]
CMD ["ls", "-a"]
# 那么它们两个会互相覆盖,谁在最后谁生效
# 容器内会默认运行以下命令
ls -a
复制指令,从本地主机复制文件或者目录到容器里指定路径。
注意
- 目标路径是绝对路径,或相对于
WORKDIR
的路径,源将在目标容器内复制到该路径中--chown
仅适用于 linux 上的 dockerfile,在 window 上没有用户、组的概念- 源文件一定要和Dockerfile文件在同一级父目录下,或者为Dockerfile父目录的子目录
COPY [--chown=<user>:<group>] <源路径1>... <目标路径>
# 用于支持包含空格的路径
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
# 将所有 hom 开头的文件复制到镜像文件系统的 /mydir/ 目录下
COPY hom* /mydir/
# ? 匹配 0 或 1 个字符,比如会把 home.txt 文件复制到 /mydir/ 目录下
COPY hom?.txt /mydir/
# 复制名为 arr[0].txt 的文件到镜像文件系统的 /mydir/ 目录下
COPY arr[[]0].txt /mydir/
# 复制 "file1" 到 `WORKDIR`/relativeDir/
COPY file1 relativeDir/
ADD
指令和 COPY
的使用格类似(同样需求下,官方推荐使用 COPY
)。功能也类似,不同之处如下:
ADD
支持添加远程 url 和自动提取压缩格式的文件,COPY
只允许从本机中复制文件COPY
支持从其他构建阶段中复制源文件(--from
)- 根据官方 Dockerfile 最佳实践,除非真的需要从远程 url 添加文件或自动提取压缩文件才用
ADD
,其他情况一律使用COPY
设置环境变量,定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。
# 设置一个变量
ENV <key> <value>
# 设置多个变量
ENV <key1>=<value1> <key2>=<value2>...
# 设置 myInfo 一个变量
ENV myInfo tyt 20 boy
# 设置多个变量
ENV name=tyt age=20 sex=boy
$varname
${varname}
${varname:-default value}
$(varname:+default value}
第一种和第二种相同
第三种表示当变量不存在使用-号后面的值
第四种表示当变量存在时使用+号后面的值
# dockerfile
FROM centos
ENV name TYT
RUN echo "第一种格式变量存在 $name"
RUN echo "第一种格式变量不存在 $username"
RUN echo "第二种格式变量存在 ${name}"
RUN echo "第二种格式变量不存在 ${username}"
RUN ["/bin/bash", "-c", "echo 第三种格式变量存在 ${name:-HSYSJ}"]
RUN ["/bin/bash", "-c", "echo 第三种格式变量不存在 ${username:-HSYSJ}"]
RUN ["sh", "-c", "echo 第四种格式变量存在 ${name:+HSYSJ}"]
RUN ["sh", "-c", "echo 第四种格式变量不存在 ${username:+HSYSJ}"]
-----------------------------------------------------------------
第一种格式 TYT
第一种格式变量不存在
第二种格式 TYT
第二种格式变量不存在
第三种格式变量存在 TYT
第三种格式变量不存在 HSYSJ
第四种格式变量存在 HSYSJ
第四种格式变量不存在
-----------------------------------------------------------------
由以上可知
第一种、第二种、第四种在变量不存在时都无法解析
声明端口配置。
作用
- 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射。
- 在运行时使用随机端口映射时,也就是
docker run -P
时,会自动随机映射EXPOSE
的端口。注意
EXPOSE
并不会让容器的端口访问到主机。要使其可访问,需要在docker run
运行容器时通过-p
来发布这些端口,或通过-P
参数来发布EXPOSE
导出的所有端口。- 如果没有暴露端口,后期也可以通过
-p 宿主机端口:容器端口
方式映射端口,但是不能通过-P
形式映射。
EXPOSE <端口1> [<端口2>...]
EXPOSE 80 443
EXPOSE 8080
EXPOSE 11211/tcp 11211/udp
定义匿名挂载数据卷。在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。
作用
- 避免重要的数据,因容器重启而丢失,这是非常致命的。
- 避免容器不断变大。
注意
一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:
- 卷可以容器间共享和重用
- 容器并不一定要和其它容器共享卷
- 修改卷后会立即生效
- 对卷的修改不会对镜像产生影响
- 卷会一直存在,直到没有任何容器在使用它
- 在启动容器
docker run
的时候,我们可以通过-v
参数修改挂载点
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
VOLUME /data
VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"]
指定工作目录。用 WORKDIR
指定的工作目录,会在构建镜像的每一层中都存在。以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR
会帮你建立目录。
docker build
构建镜像过程中的,每一个 RUN
命令都是新建的一层。只有通过 WORKDIR
创建的目录才会一直存在。
注意
- 通过
WORKDIR
设置工作目录后,Dockerfile中其后的命令RUN
、CMD
、ENTRYPOINT
、ADD
、COPY
等命令都会在该目录下执行。在使用docker run
运行容器时,可以通过-w
参数覆盖构建时所设置的工作目录。
WORKDIR <工作目录路径>
FROM centos
# 此时工作目录为/a
WORKDIR /a
RUN pwd # 输出 /a
# 此时工作目录为/a/b
WORKDIR b
RUN pwd # 输出 /a/b
# 此时工作目录为/a/b/c
WORKDIR c
RUN pwd # 输出 /a/b/c
# 此时工作目录为/d
WORKDIR /d
RUN pwd # 输出 /d
用于指定执行后续命令的用户和用户组,这边只是切换后续命令执行的用户(用户和用户组必须提前已经存在)。
注意
- 使用
USER
指定用户后,Dockerfile中其后的命令RUN
、CMD
、ENTRYPOINT
都将使用该用户。镜像构建完成后,通过docker run
运行容器时,可以通过-u
参数来覆盖所指定的用户。使用这个命令一定要确认容器中拥有这个用户,并且拥有足够权限
USER <用户名>[:<用户组>]
USER tyt
构建参数,与 ENV
作用一致。不过作用域不一样。ARG
设置的环境变量仅对 Dockerfile 内有效,也就是说只有 docker build
的过程中有效,构建好的镜像内不存在此环境变量。
构建命令 docker build
中可以用 --build-arg <参数名>=<值>
来覆盖。
ARG <参数名>[=<默认值>]
# 我们可以定义一个或多个参数
ARG username
ARG password
# 也可以给参数一个默认值
ARG username=tyt
ARG password=123
用于延迟构建命令的执行。简单的说,就是 Dockerfile 里用 ONBUILD 指定的命令,在本次构建镜像的过程中不会执行(假设镜像为 test-build)。当有新的 Dockerfile 使用了之前构建的镜像 FROM test-build
,这时执行新镜像的 Dockerfile 构建时候,会执行 test-build 的 Dockerfile 里的 ONBUILD
指定的命令。
ONBUILD <其它指令>
# 例如当前镜像 A
ONBUILD RUN ls -al
# 这个 ls -al 命令不会在A镜像构建或启动的时候执行
# 此时镜像 B 是基于镜像 A 构建的
# 那么这个ls -al 命令会在镜像 B 构建的时候被执行。
更新详细语法说明,请参考官网文档
构建步骤
编写一个dockerFile文件
使用 docker build
构建成为一个镜像
docker build -f dockerfile文件路径 -t 镜像名:[tag] .
docker run
运行镜像
docker push
发布镜像(DockerHub、阿里云镜像)
CMD # 指定这个容器启动的时候要运行的命令,仅最后一个生效,可被替代
ENTRYPOINT # 指定这个容器启动的时候要运行的命令,仅最后一个生效,可以追加命令
① 编写 dockerfile 文件
[root@iZ2vc7ujcalf4qilxwke3nZ dockerfile]# vim cmd-test
# 基于 centos
FROM centos
# 设置容器启动的时候执行 ls -a
CMD ["ls", "-a"]
② 构建成为一个镜像
docker build -f cmd-test -t cmd-test:1 .
③ 运行容器测试
dokcer run -it cmd-test:1
docker run
运行, 发现我们的ls -a
命令生效
现在,如果我们想要使用 ls -al
命令,却报错。
[root@iZ2vc7ujcalf4qilxwke3nZ dockerfile]# docker run -it cmd-test:1 -l
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "-l": executable file not found in $PATH: unknown.
ERRO[0000] error waiting for container: context canceled
这是因为当我们添加了 -l
参数后,-l
替换了CMD["ls", "-a"]
命令, -l
不是命令,所以报错了。如果我们想要追加一个-l
,就可以考虑使用ENTRYPOINT
。
① 编写 dockerfile 文件
[root@iZ2vc7ujcalf4qilxwke3nZ dockerfile]# vim point-test
# 基于 centos
FROM centos
# 设置容器启动的时候执行 ls -a
ENTRYPOINT ["ls", "-a"]
② 构建成为一个镜像
docker build -f point-test -t point-test:1 .
③ 运行容器测试
dokcer run -it point-test:1
docker run
运行, 与 CMD相同,发现我们的ls -a
命令生效
现在,我们追加一个 -l
参数
我们的追加命令, 是直接拼接到ENTRYPOINT
命令的后面,即 ls -al
demo-df
)# 指定基础镜像
FROM centos
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local
# 指定工作目录
WORKDIR /tmp
# 拷贝jdk和java项目的包,这两个文件都在 demo-df 同目录
COPY ./jdk8.tar.gz $JAVA_DIR/
# 会将 springboot-demo.jar 复制到容器内 /tmp目录下,并命名为 app.jar
COPY ./springboot-demo.jar app.jar
# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8
# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin
# 暴露端口
EXPOSE 8080
# 入口,java项目的启动命令
# 启动应用固定命令
ENTRYPOINT ["java" , "-jar" ]
# 动态传递jar包名,启动容器内 /tmp 目录下的app.jar
CMD ["app.jar"]
docker build -f demo-df -t demo:1 .
docker run -id --name demo1 -p 8080:8080 demo:1
虽然我们可以基于Centos基础镜像,添加任意自己需要的安装包,构建镜像,但是却比较麻烦。所以大多数情况下,我们都可以在一些安装了部分软件的基础镜像上做改造。
例如,构建Java项目的镜像,可以在已经准备了JDK的基础镜像基础上构建。
需求:基于java:8-alpine
镜像,将一个Java项目构建为镜像。
demo-df2
)# 指定基础镜像
FROM java:8-alpine ·
# 拷贝java项目的包,该文件在 demo-df2 同目录
# 拷贝到 /tmp/app.jar
COPY ./springboot-demo.jar /tmp/app.jar
# 暴露端口
EXPOSE 8081
# 入口,java项目的启动命令,启动容器内 /tmp 目录下的app.jar
ENTRYPOINT java -jar app.jar
docker build -f demo-df2 -t demo2:1 .
docker run -id --name demo2 -p 8081:8081 demo2:1
DockerHub注册账号
确定账号可以登录
docker login -u 账号名
docker push 镜像名:tag
例
# 此处我将刚才制作的 Java项目 镜像推送上去
docker push demo2:1
但是报错
[root@iZ2vc772r7963j25lx3cusZ test]# docker push demo2:1
The push refers to repository [docker.io/library/demo2]
3dba9e1d09df: Preparing
35c20f26d188: Preparing
c3fe59dd9556: Preparing
6ed1a81ba5b6: Preparing
a3483ce177ce: Preparing
ce6c8756685b: Waiting
30339f20ced0: Waiting
0eb22bfb707d: Waiting
a2ae92ffcd29: Waiting
denied: requested access to the resource is denied # 拒绝
这里大多数是两种原因
① docker未登录
解决方法:docker login
② 如果已经登陆了还是报错,应将镜像改到自己账户名下
在上述制作 demo2 镜像中时,我指定的MAINTAINER
为 tyt
,但是我的DockerHub的账号名为tangyt6
。
解决方法:
docker tag demo2:1 tangyt6/demo2:1
docker push tangyt6/demo2:1
[root@iZ2vc772r7963j25lx3cusZ test]# docker push tangyt6/demo2:1
The push refers to repository [docker.io/tangyt6/demo2]
3dba9e1d09df: Pushed
35c20f26d188: Pushed
c3fe59dd9556: Pushed
6ed1a81ba5b6: Pushed
a3483ce177ce: Pushed
ce6c8756685b: Pushed
30339f20ced0: Pushed
0eb22bfb707d: Pushed
a2ae92ffcd29: Pushed
1: digest: sha256:6cc28f078ab5f94d9c4178d2c985b402dc40b81d0c3df84a78cb2bda67b53aba size: 2212
# 成功