Dockerfile学习总结

警告
本文最后更新于 2023-02-28,文中内容可能已过时,请谨慎使用。

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。FROM 就是指定基础镜像,一个 DockerfileFROM 是必备的指令,并且必须是第一条指令

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

除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

FROM scratch
...

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

WORKDIR /app

如果你的 WORKDIR 指令使用的相对路径,那么所切换的路径与之前的 WORKDIR 有关:

FROM ubuntu:latest
WORKDIR /a
WORKDIR b
WORKDIR c

RUN pwd

RUN pwd 的工作目录为 /a/b/c

RUN和CMD区别

RUN是构建容器时就运行的命令以及提交运行结果

CMD是容器启动时执行的命令,在构建时并不运行

RUN执行命令可以有下面两种格式:

  • shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。
RUN ["pip3", "install","-r","requirements.txt"]

Dockerfile中每一个指令都会建立一层, 像下面编译、安装redis可执行文件只需要在一层处理。这时候只需要使用一个 RUN 指令,并使用 && 将各个所需命令串联起来即可。

为了确保最后的镜像不会太臃肿,应该在安装之后清理无用的安装包

FROM debian:stretch

RUN set -x; buildDeps='gcc libc6-dev make wget' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

格式: COPY [--chown=<user>:<group>] <源路径>... <目标路径>

COPY /dir /app

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

# error
COPY ../test.txt /app

上述的命令会报错,因为docker不支持拷贝上下文目录的父目录及其文件 ! ! !

注意
ADDCOPY指令支持更多功能,但是还是建议使用COPY复制文件,下面给出一个使用ADD的特例: 自动解压缩

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

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

CMD设置container启动时执行的操作

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

  • shell 格式:CMD <命令>

  • exec 格式:CMD ["可执行文件", "参数1", "参数2"...]

Docker不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。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 数组,因此一定要使用双引号 "而不要使用单引号

ENTERYPOINT设置container启动时执行的操作

前面CMD命令的exec格式为: CMD ["可执行文件", "参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。CMD命令的格式变为:CMD ["参数1", "参数2"...]

问题
有了CMD后,为什么还要有ENTRYPOINT呢?

场景: 让镜像变成像命令一样使用

假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:

FROM ubuntu:latest
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://myip.ipip.net" ]

假如我们使用 docker build -t myip . 来构建镜像的话,如果我们需要查询当前公网IP,只需要执行:

$ docker run myip
当前 IP:119.145.72.133  来自于:中国 广东 广州  电信

嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 CMD 中可以看到实质的命令是 curl,那么如果我们希望显示 HTTP 头信息,就需要加上 -i 参数。那么我们可以直接加 -i 参数给 docker run myip 么?

$ docker run myip -i
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "-i": executable file not found in $PATH: unknown.
ERRO[0000] error waiting for container: context canceled

我们可以看到可执行文件找不到的报错,executable file not found。之前我们说过,跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的 CMD,而不是添加在原来的 curl -s http://myip.ipip.net 后面。而 -i 根本不是命令,所以自然找不到。

那么如果我们希望加入 -i 这参数,我们就必须重新完整的输入这个命令:

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

这显然不是很好的解决方案,而使用 ENTRYPOINT 就可以解决这个问题。现在我们重新用 ENTRYPOINT 来实现这个镜像:

FROM ubuntu:latest
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 -i

$ docker run myip -i
HTTP/1.1 200 OK
Date: Thu, 23 Feb 2023 13:43:06 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 69
Connection: keep-alive
X-Cache: BYPASS
X-Request-Id: 43daac3150443f0d710698abe0f18848
Server: WAF
Connection: close
Accept-Ranges: bytes

当前 IP:119.145.72.133  来自于:中国 广东 广州  电信

可以看到,这次成功了。这是因为当存在 ENTRYPOINT 后,CMD 的内容将会作为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,因此会作为参数传给 curl,从而达到了我们预期的效果。

格式有两种:

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

设置多个环境变量,中间有空格需要用""包裹。

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

使用环境变量

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

下面的命令创建了两个挂载点/data1/data2。通过VOLUME 指令创建的挂载点,无法指定主机上对应的目录,是自动生成的。可以通过docker volume inspect查看

volume ["/data1","/data2"]

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

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

真正的暴露端口是在创建容器run的时候指定的 -p <宿主端口>:<容器端口> 或者 -P参数

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。

ARG指令有生效范围,如果在FROM指令之前指定,那么只能用于FROM指令中。

ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

RUN set -x ; echo ${DOCKER_USERNAME}

FROM指令之后要使用ARG变量必须再次使用 ARG指令指定

# 只在FROM中生效
ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

# 要想在FROM之后使用,必须再次指定
ARG DOCKER_USERNAME=library

RUN set -x ; echo ${DOCKER_USERNAME}

多阶段构建使用:对于在各个阶段中使用的变量都必须在每个阶段分别指定

ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

# 在FROM之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library

RUN set -x ; echo ${DOCKER_USERNAME}

FROM ${DOCKER_USERNAME}/alpine

# 在FROM之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library

RUN set -x ; echo ${DOCKER_USERNAME}

用于指定运行镜像所使用的用户:

USER user

使用USER指定用户后,Dockerfile 中其后的命令RUNCMDENTRYPOINT都将使用该用户。镜像构建完成后,通过docker run运行容器时,可以通过-u参数来覆盖所指定的用户

使用 Dockerfile 构建镜像时最好是将 Dockerfile 放置在一个新建的空目录下。然后将构建镜像所需要的文件添加到该目录中。为了提高构建镜像的效率,你可以在目录下新建一个 .dockerignore 文件来指定要忽略的文件和目录。.dockerignore 文件的排除模式语法和 git.gitignore 文件相似

下面是一个示例:

.idea/
.git/

vendor/

node_modules/

public/js/
public/css/
public/mix-manifest.json

yarn-error.log

bootstrap/cache/*
storage/
docker build -t tag_name .

最后面的.设置当前目录为docker构建的上下文目录,不指定Dockerfile路径时会默认在当前目录寻找Dockerfile文件,你也可以手动指定-f ../Dockerfile.txt作为Dockerfile

很多命令如COPYADD等指令中的源文件的路径都是基于上下文目录的相对路径

信息
习惯上我们将Dockerfile置于项目根目录,并指定该目录为docker构建的上下文目录

每一条FROM指令都是一个构建阶段,多条 FROM就是多阶段构建

通过多阶段构建,您可以在Dockerfile中使用多个FROM语句。每个FROM指令可以使用不同的基础,并且每个都开始构建的新阶段。您可以有选择地将前面阶段的构建出的文件复制到后面阶段,从而在最终image中留下不需要的所有内容。

例如:

FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]

只构建某一阶段的镜像

默认情况下,未命名阶段,您可以通过它们的整数来引用它们,对于第一个FROM指令,可以使用0来引用。

COPY --from=0 /go/src/github.com/alexellis/href-counter/app .

我们还可以使用 as 来为某一阶段命名,例如

FROM golang:alpine as builder

例如当我们只想构建 builder 阶段的镜像时,增加 --target=builder 参数即可

$ docker build --target builder -t username/imagename:tag .

构建时从其他镜像复制文件

从前面的builder阶段构建的镜像中复制文件

COPY --from=builder /go/src/github.com/go/helloworld/app .

也可以复制任意镜像中的文件

COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

1.直接用Git repo进行构建

docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world

这行命令指定了构建所需的 Git repo,并且指定分支为 master,构建目录为 /amd64/hello-world/,然后 Docker 就会自己去 git clone 这个项目、切换到指定分支、并进入到指定目录后开始构建。

2.用给定的tar压缩包构建

docker build https://server/context.tar.gz

如果所给出的 URL 不是个Git repo,而是个 tar 压缩包,那么Docker引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。


相关文章