Dockerfile基础命令及简单应用

发布时间 2023-05-01 23:11:25作者: Cyrui_13

Dockerfile

docker commit 的学习中,我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

初识 Dockerfile

以之前定制 nginx 镜像为例,这次我们使用 Dockerfile 来定制。

在一个空白目录中,建立一个文本文件,并命名为 Dockerfile

mkdir docker-test-volume
cd docker-test-volume
vim Dockerfile

Dockerfile 的内容如下:

FROM nginx
RUN echo 'Hello world!' > /usr/share/nginx/html/index.html

image

然后我们利用 Dockerfile 来构建镜像!

docker build -t nginx:v3 .

image

然后我们运行新建的镜像

docker run --name my-nginx -d -p 80:80 nginx:v3

image

出现这个界面,说明我们的镜像成功启动了!

image

Dockerfile 命令

FROM 指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。而 FROM 就是指定 基础镜像,因此一个 DockerfileFROM 是必备的指令,并且必须是第一条指令。

在 Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginxredismongomysqlhttpdphptomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 nodeopenjdkpythonrubygolang 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。

除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

FROM [--platform=<platform>] <image> [AS <name>]
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
FROM centos:7	# 例子

MAINTAINER 设置 author

该命令已废弃,我们可以使用 LABEL 来设置 author

LABEL org.opencontainers.image.authors="cyr@gmail.com"

RUN 执行命令

RUN指令将在当前镜像上的新层中执行任何命令并提交结果。生成的提交镜像将用于Dockerfile中的下一步。其格式有两种:

  • shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。
  • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。

需要注意的是,Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,就会新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。

COPY 复制文件

格式:

  • COPY [--chown=<user>:<group>] <源路径>... <目标路径>
  • COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。

COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。比如:

COPY package.json /usr/src/app/

<源路径> 可以是多个,甚至可以是通配符,如:

COPY hom* /mydir/
COPY hom?.txt /mydir/

如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。

ADD 更高级的复制文件

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。

如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。

FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /

如果 <源路径> 是一个 URL,Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去。下载后的文件权限自动设置为 600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用

CMD 容器启动命令

CMD 指令的格式和 RUN 相似,也是两种格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
  • 参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 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" ]

这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。

ENTRYPOINT 入口点

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。

当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:

<ENTRYPOINT> "<CMD>"

举个例子:

假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 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 myip 

这时如果我们要加入参数 -i 来显示HTTP头信息,我们试着运行下面命令

docker build 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".

这里显示报错了,原因是这里的-i替换了原来的CMD,而不是在原来的基础后面加入-i,我们需要输入命令才行

docker build myip curl -s http://myip.ipip.net -i

我们再用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
当前 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在这里会更为方便。

ENV 设置环境变量

格式有两种:

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...

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

ENV VERSION=1.0 DEBUG=on NAME="Happy Feet"

比如在官方 node 镜像 Dockerfile 中,就有类似这样的代码:

ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
  && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
  && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
  && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
  && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
  && ln -s /usr/local/bin/node /usr/local/bin/nodejs

VOLUME 定义匿名卷

格式为:

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

之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

VOLUME /data
VOLUME ["data1", "data2"]

这里的 /data 目录就会在容器运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行容器时可以覆盖这个挂载设置。比如:

$ docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

EXPOSE 暴露端口

格式为 EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

WORKDIR 指定工作目录

格式为 WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。

之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:

RUN cd /app
RUN echo "hello" > world.txt

如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是 hello。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。

之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。

因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。

WORKDIR /app
RUN echo "hello" > world.txt

LABEL 为镜像添加元数据

LABEL 指令用来给镜像以键值对的形式添加一些元数据(metadata)。

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

我们还可以用一些标签来申明镜像的作者、文档地址等:

LABEL org.opencontainers.image.authors="cyr"
LABEL org.opencontainers.image.documentation="https://cyr.github.io"

实战:构建自己的 centos 镜像

原先的 centos 镜像无法执行vimifconfig指令

image

我们自己构建一个centos,来实现这两个指令,首先我们写 Dockerfile

FROM centos:7
LABEL author="cyr@gmail.com"

ENV MYPATH /usr/local
WORKDIR $MYPATH

RUN yum -y install vim
RUN yum -y install net-tools

EXPOSE 80

CMD echo $MYPATH
CMD echo "=======end========"
CMD /bin/bash

image

接着我们构建 Dockerfile 生成镜像

docker build -f Dockerfile -t mycentos:0.1 .

image

测试运行一下:

docker run -it mycentos:0.1

image

可以发现,指令运行成功了!

实战:制作tomcat镜像

  1. 新建文件夹mytomcat
  2. 将下载好的jdk8tomcat9放到mytomcat目录下
  3. 新建 readme.txtDockerfile文件
  4. 构建镜像
  5. 启动容器
  6. 访问测试
  7. 添加web.xmlWEB-INFa.jsp,重启容器访问自己的页面
# 准备文件
[root@localhost home]# mkdir /home/mytomcat
[root@localhost home]# cd mytomcat/
[root@localhost mytomcat]# touch readme.txt
[root@localhost mytomcat]# vim Dockerfile
[root@localhost mytomcat]# ll
总用量 147336
-rw-r--r--. 1 root root  11642299 5月   1 14:23 apache-tomcat-9.0.74.tar.gz
-rw-r--r--. 1 root root       914 5月   1 14:32 Dockerfile
-rw-r--r--. 1 root root 139219380 5月   1 14:23 jdk-8u371-linux-x64.tar.gz
-rw-r--r--. 1 root root         0 5月   1 14:28 readme.txt
[root@localhost mytomcat]# cat Dockerfile 
FROM centos:7
MAINTAINER cyr<cyr@gmail.com>
#把宿主机当前上下文的readme.txt拷贝到容器/usr/local/路径下
COPY readme.txt /usr/local/cincontainer.txt
#把java与tomcat添加到容器中
ADD jdk-8u371-linux-x64.tar.gz /usr/local/
ADD apache-tomcat-9.0.74.tar.gz /usr/local/
#安装vim编辑器
RUN yum -y install vim
#设置工作访问时候的WORKDIR路径,登录落脚点
ENV MYPATH /usr/local
WORKDIR $MYPATH
#配置java与tomcat环境变量
ENV JAVA_HOME /usr/local/jdk1.8.0_371
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV CATALINA_HOME /usr/local/apache-tomcat-9.0.74
ENV CATALINA_BASE /usr/local/apache-tomcat-9.0.74
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin
#容器运行时监听的端口
EXPOSE  8080
#启动时运行tomcat
CMD /usr/local/apache-tomcat-9.0.74/bin/startup.sh && tail -F /usr/local/apache-tomcat-9.0.74/bin/logs/catalina.out

# 构建镜像
[root@localhost mytomcat]# docker build -t mytomcat .
[+] Building 44.4s (11/11) FINISHED                           
 => [internal] load build definition from Dockerfile            
 => => transferring dockerfile: 1.01kB                           
 => [internal] load .dockerignore                                 
 => => transferring context: 2B                                  
 => [internal] load metadata for docker.io/library/centos:7      
 => [internal] load build context                                
 => => transferring context: 150.90MB                             
 => CACHED [1/6] FROM docker.io/library/centos:7@sha256:9d4bcbbb213dfd745b58be38b13b996ebb5ac315fe75711bd618426a630e0987 
 => [2/6] COPY readme.txt /usr/local/cincontainer.txt            
 => [3/6] ADD jdk-8u371-linux-x64.tar.gz /usr/local/            
 => [4/6] ADD apache-tomcat-9.0.74.tar.gz /usr/local/          
 => [5/6] RUN yum -y install vim                              
 => [6/6] WORKDIR /usr/local                                  
 => exporting to image                                       
 => => exporting layers                                       
 => => writing image sha256:ca383e6a4bed230b3abe065830e7f4d717458f31ef609e67c43ed564e387e544     
 => => naming to docker.io/library/mytomcat

# 启动容器 (这里之前一直启动失败,我的原因是挂载的路径有点问题,未解之谜)
[root@localhost local]# docker run -d -p 9000:8080 --name mytomcat1 -v /home/mytomcat/test:/usr/local/apache-tomcat-9.0.74/webapps/test mytomcat
51655608be591c1aa2cf5acdffce9236c2a3de13df3a8534a6809efaa72abc37
[root@localhost local]# docker ps
CONTAINER ID   IMAGE      COMMAND                   CREATED         STATUS         PORTS                                       NAMES
51655608be59   mytomcat   "/bin/sh -c '/usr/lo…"   3 seconds ago   Up 2 seconds   0.0.0.0:9000->8080/tcp, :::9000->8080/tcp   mytomcat1

访问测试结果

image

容器内的test挂载到宿主机的test上

image

[root@localhost mytomcat]# cd test/
[root@localhost test]# vim web.xml
[root@localhost test]# mkdir WEB-INF
# [root@localhost test]# cd WEB-INF/
[root@localhost WEB-INF]# vim a.jsp		# 我这里的a.jsp 必须与 WEB-INF 同级才能访问,未解之谜

[root@localhost WEB-INF]# cat a.jsp 
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Insert title here</title>
  </head>
  <body>
    -----------welcome------------
    <%="i am in docker tomcat self "%>
    <br>
    <br>
    <% System.out.println("=============docker tomcat self");%>
  </body>
</html>

[root@localhost WEB-INF]# cat ../web.xml 
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://java.sun.com/xml/ns/javaee"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
  id="WebApp_ID" version="2.5">
  
  <display-name>test</display-name>
 
</web-app>


# 重启容器
docker restart 6400		# 6400 为容器id简略版

下面是访问结果!

image