关于Dockerfile
在Docker中创建镜像最常用的方式,就是使用Dockerfile。Dockerfile是一个Docker镜像的描述文件,我们可以理解成火箭发射的A、B、C、D…的步骤。Dockerfile其内部包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
一个Dockerfile的示例如下所示:
#基于centos镜像
FROM centos
#维护人的信息
MAINTAINER The CentOS Project
#安装httpd软件包
RUN yum -y update
RUN yum -y install httpd
#开启80端口
EXPOSE 80
#复制网站首页文件至镜像中web站点下
COPY index.html /var/www/html/index.html
#复制该脚本至镜像中,并修改其权限
COPY run.sh /run.sh
RUN chmod 775 /run.sh
#当启动容器时执行的脚本文件
CMD ["/run.sh"]
由上可知,Dockerfile结构大致分为四个部分:
(1)基础镜像信息
(2)维护者信息
(3)镜像操作指令
(4)容器启动时执行指令。
Dockerfile每行支持一条指令,每条指令可带多个参数,支持使用以#号开头的注释。下面会对上面使用到的一些常用指令做一些介绍。
DockerFile构建过程解析
基础知识
- 每条保留字指令都必须为大写字母后面要跟随至少一个参数
- 指令从上到下顺序执行
- #表示注释
- 每条指令都会创建一个新的镜像层,并对镜像进行提交
大致流程
- docker从基础镜像运行一个容器
- 执行一条指令并对容器进行修改
- 执行类似于docker commit的操作提交一个新的镜像
- docker再基于别提交的新的镜像运行一个新的容器
- 执行dockerfile的下一个指令再从执行第2点直到没有指令
构建镜像对的命令
docker build -t nginx:v1 .
docker build -t 构建镜像 nginx:v1 构建镜像的名称和标签 . 代表当前目录(不完全正确,这里指的上下文)
※举两个例子:
错误用法 copy ../index.html /app (超越了上下文路径)
正确用法 copy ./test.html /app
首先我们要理解 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常用指令
FROM:指明构建的新镜像是来自于哪个基础镜像,例如:
FROM centos:6
From:选择基础镜像,选择基础镜像的时候就需要去考虑到底需要什么样的基础镜像,需要完备的操作系统呢,比如from unbuntu centos?
完备操作系统好处是什么指令都有,很方便,但是缺点也明显,第一就是很大,它传输效率就慢,同时占用了磁盘空间,对存储的要求就高了。第二,更严重的问题就是,万一你的base image里面有安全漏洞,包含了很多工具,这样会对你的生产系统造成破坏。
但是不是基于操作系统,比如基于stratch,那么你想进入容器调试容器,那么就丧失了这种能力,没有这些工具。
所以docker会推荐有个小的基础镜像,比如apline,也就是一个小的linux,然后在这个基础之上,需要什么工具,你可以使用命令基本的安装一下,这样就具备了基本的调试能力,之后在这基础之上安装你的应用。当然啥都不装也是可以的,那可以去主机上面做debug。
通过lables可以将容器镜像,打上各种不同的标签,这个标签作用就是通过docker images命令去查看的时候可以通过-f的参数来filter来查询某类容器镜像,容器镜像比较多的时候还是很有用的。
RUN就是用来执行一些指令的,如果要去安装一个中间件的时候,这两条指令RUN apt-get update && apt-install连起来,如果分开执行,apt-agt update就会被缓存,新的包在下次执行的时候它就不会在更新了。后面的install就会失败。
容器最后运行什么命令使用CMD,这里面可以指定可执行文件是什么,所定义的参数是什么。
MAINTAINER:指明镜像维护着及其联系方式(一般是邮箱地址),例如:
MAINTAINER Edison Zhou <edisonchou@hotmail.com>
RUN:构建镜像时运行的Shell命令,例如:
RUN ["yum", "install", "httpd"]
RUN yum install httpd
又如,我们在使用微软官方ASP.NET Core Runtime镜像时往往会加上以下RUN命令,弥补无法在默认镜像下使用Drawing相关接口的缺憾:
FROM microsoft/dotnet:2.2.1-aspnetcore-runtime
RUN apt-get update
RUN apt-get install -y libgdiplus
RUN apt-get install -y libc6-dev
RUN ln -s /usr/lib/libgdiplus.so /lib/x86_64-linux-gnu/libgdiplus.so
CMD:启动容器时执行的Shell命令,例如:
CMD ["-C", "/start.sh"]
CMD ["/usr/sbin/sshd", "-D"]
CMD /usr/sbin/sshd -D
CMD类似于RUN的命令,用于运行程序,但二者的运行时间不同:
-
CMD 在docker run 时运行。
-
RUN 是在 docker build。
EXPOSE:声明容器运行的服务端口,例如:
EXPOSE 80 443
expose用处不是特别大,就是发布端口,这样用处其实不是特别大,即使你不通过expose这个指令,事实上你的应用监听在80端口,那么80端口就能够对外访问。expose是镜像提供者告诉使用镜像的人服务发布的端口。
EXPOSE指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务.
在Dockerfile中写入这样的声明有两个好处:
-
帮助镜像使用者理解这个镜像服务的端口,以方便配置映射;
-
另个用处是在运行时使用随机端口映射时,也就是 docker run -P,会自动随机映射
启动tomcat:docker run -p 8080:8080 tomcat
-P:左边宿主机端口,右边容器端口
-p:宿主机段端口随机分配一个端口,让外部进行访问
ENV:设置环境内环境变量,例如:
ENV MYSQL_ROOT_PASSWORD 123456
ENV JAVA_HOME /usr/local/jdk1.8.0_45
ADD:拷贝文件或目录到镜像中,例如:
ADD <src>...<dest>
ADD html.tar.gz /var/www/html
ADD https://xxx.com/html.tar.gz /var/www/html
ADD 命令和COPY命令差不多,比COPY高级
-
ADD 的优点:在执行 <源文件> 为 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,会自动复制并解压到 <目标路径>。
-
ADD 的缺点:在不解压的前提下,无法复制 tar 压缩文件。会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。具体是否使用,可以根据是否需要自动解压来决定。
-
PS:如果是URL或压缩包,会自动下载或自动解压。
处理from去指定一个image,除了apt-get去安装一个中间件,很多时候还是要从构建好的可执行文件加入到镜像里面,这里就需要拷贝和添加的动作。
在add时候可以改变文件的权限,最终就是告诉源文件和目标文件是什么。
值得注意的是ADD不是简单拷贝复制的动作,还有些附加的动作,比如本地是压缩文件,它在ADD的时候就可以去解压,所以不是那么直观。
COPY:拷贝文件或目录到镜像中,用法同ADD,只是不支持自动下载和解压,例如:
COPY ./start.sh /start.sh
COPY 复制文件:
COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"] 目标路径也可以成为镜像的目录
COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。
例如:COPY package.json /usr/src/app/
ENTRYPOINT:
entrypoints上面是可执行文件 如果 这样写
ENTRYPOINT ["/bin/java -jar demo.jar"]
CMD ["-Xmx512m -Xms512m","--server.port=8000"]会报错 unable to start container process: exec: "/bin/java -jar demo.jar": stat /bin/java -jar demo.jar: no such file or directory: unknown.
因为/bin/java -jar demo.jar这不是一条可执行文件
entrypoint才是让容器面向应用的一个主要因素,容器镜像是面向应用的,一般生产系统里面不会有centos镜像跑在那,要不是http server,以及各种各样的服务。
所以容器镜像是面向某个应用的,entrypoint指定启动容器的时候要启动哪些服务,有了这个之后,容器镜像在加载之后不仅仅是一个操作系统,它可以是操作系统+应用,又可以是没有操作系统只有应用,所以是面向应用构建的容器镜像。
entrypoint一般是dockerfile的最后一条指令。
entrypoint最佳实践是:entrypoint定义主命令,cmd来定义它主要参数,这样在容器启动时候就是启动entrypoint这个命令并且附加cmd的参数。
启动容器时执行的Shell命令,同CMD类似,只是由ENTRYPOINT启动的程序不会被docker run命令行指定的参数所覆盖,而且,这些命令行参数会被当作参数传递给ENTRYPOINT指定指定的程序,例如:
ENTRYPOINT ["/bin/bash", "-C", "/start.sh"]
ENTRYPOINT /bin/bash -C '/start.sh'
PS:Dockerfile文件中也可以存在多个ENTRYPOINT指令,但仅有最后一个会生效。
VOLUME:指定容器挂载点到宿主机自动生成的目录或其他容器,例如:
VOLUME ["/var/lib/mysql"]
PS:一般不会在Dockerfile中用到,更常见的还是在docker run的时候指定-v数据卷。
作用:Docker容器的数据持久化.
定义:数据卷是一个可以提供一个或多个容器使用的特殊目录,它绕过UFS
特性:
- 数据卷可以在容器之间共享和重用
- 对数据卷的修改会立即生效
- 对数据卷的更新,不会影响镜像
- 数据卷会默认一直存在,即使容器被删除
- 命令:-v数据卷启动
docker run -p 8080:8080 --name tomcat -d -v usr/local/docker/tomcat/ROOT:usr/local/tomcat/webapps/ROOT tomcat
USER:为RUN、CMD和ENTRYPOINT执行Shell命令指定运行用户,例如:
USER <user>[:<usergroup>]
USER <UID>[:<UID>]
USER edisonzhou
WORKDIR:为RUN、CMD、ENTRYPOINT以及COPY和AND设置工作目录,例如:
WORKDIR /data
HEALTHCHECK:告诉Docker如何测试容器以检查它是否仍在工作,即健康检查,例如:
HEALTHCHECK --interval=5m --timeout=3s --retries=3 \
CMD curl -f http:/localhost/ || exit 1
其中,一些选项的说明:
-
--interval=DURATION (default: 30s):每隔多长时间探测一次,默认30秒
-
-- timeout= DURATION (default: 30s):服务响应超时时长,默认30秒
-
--start-period= DURATION (default: 0s):服务启动多久后开始探测,默认0秒
-
--retries=N (default: 3):认为检测失败几次为宕机,默认3次
一些返回值的说明:
-
0:容器成功是健康的,随时可以使用
-
1:不健康的容器无法正常工作
-
2:保留不使用此退出代码
ARG 作用范围
Docker中传递变量主要使用ARG和ENV,虽然功能相同,但是他们的作用范围是不一样的。
ARG传递变量:
ARG只在Dockerfile中生效,且在docker build阶段生效,构建好的镜像内不存在此环境变量。意味着在容器启动后ARG定义的变量已经无效,如果想让其生效,需要将其赋值给ENV。
#Dockerfile
From java
VOLUME /tmp
ARG JAR_FILE=*.jar
COPY ${JAR_FILE} helloworld.jar
ARG OPTS="-Xmx512m -Xms512m -Dspring.profiles.active=test"
ENV JAVA_OPTS=${OPTS}
ENTRYPOINT ["/bin/sh","-c","java $JAVA_OPTS -jar /helloworld.jar"]
#build
docker build -t helloworld:v1 .
#run
docker run -p 8080:8080 --name helloworld helloworld:v1
#docker exec -it 29a90e0be2b3 /bin/bash
root@29a90e0be2b3:/# ps -ef|grep java
root 6 1 10 13:22 ? 00:00:07 java -Xmx512m -Xms512m -Dspring.profiles.active=test -jar /helloworld.jar
在构建镜像时,指定一些参数,例如:
FROM centos:6
ARG user # ARG user=root
USER $user
这时,我们在docker build时可以带上自定义参数user了,如下所示:
docker build --build-arg user=edisonzhou Dockerfile .
综合Dockerfile案例
下面是一个Java Web应用的镜像Dockerfile,综合使用到了上述介绍中最常用的几个命令:
FROM centos:7
MAINTANIER www.edisonchou.com
ADD jdk-8u45-linux-x64.tar.gz /usr/local
ENV JAVA_HOME /usr/local/jdk1.8.0_45
ADD apache-tomcat-8.0.46.tar.gz /usr/local
COPY server.xml /usr/local/apache-tomcat-8.0.46/conf
RUN rm -f /usr/local/*.tar.gz
WORKDIR /usr/local/apache-tomcat-8.0.46
EXPOSE 8080
ENTRYPOINT ["./bin/catalina.sh", "run"]
有了Dockerfile,就可以创建镜像了:
docker build -t edc-tomcat:v1 .
最后,可以通过以下命令创建容器:
docker run -itd --name=tomcate -p 8080:8080 \
-v /app/webapps/:/usr/local/apache-tomcat-8.0.46/webapps/ \
edc-tomcat:v1