# Dockerfile学习总结
### FROM-指定基础镜像
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。`FROM` 就是指定**基础镜像**,一个 `Dockerfile` 中 `FROM` 是必备的指令,**并且必须是第一条指令**。
在[Docker Hub](https://hub.docker.com/search?q=&type=image&image_filter=official) 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 [`nginx`](https://hub.docker.com/_/nginx/)、[`redis`](https://hub.docker.com/_/redis/)、[`mongo`](https://hub.docker.com/_/mongo/)、[`mysql`](https://hub.docker.com/_/mysql/)、[`httpd`](https://hub.docker.com/_/httpd/)、[`php`](https://hub.docker.com/_/php/)、[`tomcat`](https://hub.docker.com/_/tomcat/) 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 [`node`](https://hub.docker.com/_/node)、[`openjdk`](https://hub.docker.com/_/openjdk/)、[`python`](https://hub.docker.com/_/python/)、[`ruby`](https://hub.docker.com/_/ruby/)、[`golang`](https://hub.docker.com/_/golang/) 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 `scratch`。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。如果你以 `scratch` 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
```dockerfile
FROM scratch
...
```
### WORKDIR-指定工作目录
使用 `WORKDIR` 指令可以来指定工作目录(或者称为当前目录),**以后各层的当前目录就被改为指定的目录**,如该目录不存在,`WORKDIR` 会帮你建立目录。
```dockerfile
WORKDIR /app
```
如果你的 `WORKDIR` 指令使用的相对路径,那么所切换的路径与之前的 `WORKDIR` 有关:
```dockerfile
FROM ubuntu:latest
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
```
`RUN pwd` 的工作目录为 `/a/b/c`。
### RUN-执行命令
{{< admonition title="RUN和CMD区别" >}}
`RUN`是构建容器时就运行的命令以及提交运行结果
`CMD`是容器启动时执行的命令,在构建时并不运行
{{< /admonition >}}
`RUN`执行命令可以有下面两种格式:
- *shell* 格式:`RUN <命令>`,就像直接在命令行中输入的命令一样
```dockerfile
RUN echo '
Hello, Docker!
' > /usr/share/nginx/html/index.html
```
- *exec* 格式:`RUN ["可执行文件", "参数1", "参数2"]`,这更像是函数调用中的格式。
```dockerfile
RUN ["pip3", "install","-r","requirements.txt"]
```
`Dockerfile`中每一个指令都会建立一层, 像下面编译、安装`redis`可执行文件只需要在一层处理。这时候只需要使用一个 `RUN` 指令,并使用 `&&` 将各个所需命令串联起来即可。
为了确保最后的镜像不会太臃肿,应该在安装之后清理无用的安装包
```dockerfile
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-复制文件
格式: `COPY [--chown=:] <源路径>... <目标路径>`
```dockerfile
COPY /dir /app
```
如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径
```dockerfile
# error
COPY ../test.txt /app
```
上述的命令会报错,因为`docker`不支持拷贝上下文目录的父目录及其文件 ! ! !
### ADD-更高级的复制文件
{{< admonition >}}
`ADD`比`COPY`指令支持更多功能,但是还是建议使用`COPY`复制文件,下面给出一个使用`ADD`的特例: 自动解压缩
{{< /admonition >}}
如果 `<源路径>` 为一个 `tar` 压缩文件的话,压缩格式为 `gzip`, `bzip2` 以及 `xz` 的情况下,`ADD` 指令将会自动解压缩这个压缩文件到 `<目标路径>` 去。
```dockerfile
FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...
```
### CMD-容器启动命令
`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-入口点
`ENTERYPOINT`设置`container`启动时执行的操作
前面`CMD`命令的`exec`格式为: `CMD ["可执行文件", "参数1", "参数2"...]`。在指定了 `ENTRYPOINT` 指令后,用 `CMD` 指定具体的参数。`CMD`命令的格式变为:`CMD ["参数1", "参数2"...]`。
{{< admonition type="question">}}
有了`CMD`后,为什么还要有`ENTRYPOINT`呢?
{{< /admonition >}}
> **场景: 让镜像变成像命令一样使用**
假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 `CMD` 来实现:
```dockerfile
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`,只需要执行:
```sh
$ docker run myip
当前 IP:119.145.72.133 来自于:中国 广东 广州 电信
```
嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 `CMD` 中可以看到实质的命令是 `curl`,那么如果我们希望显示 HTTP 头信息,就需要加上 `-i` 参数。那么我们可以直接加 `-i` 参数给 `docker run myip` 么?
```sh
$ 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` 这参数,我们就必须重新完整的输入这个命令:
```sh
$ docker run myip curl -s http://myip.ipip.net -i
```
这显然不是很好的解决方案,而使用 `ENTRYPOINT` 就可以解决这个问题。现在我们重新用 `ENTRYPOINT` 来实现这个镜像:
```dockerfile
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`:
```sh
$ 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环境变量
格式有两种:
- `ENV `
- `ENV = =...`
设置多个环境变量,中间有空格需要用`""`包裹。
```dockerfile
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"
```
使用环境变量
```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-创建挂载点
下面的命令创建了两个挂载点`/data1`和`/data2`。通过`VOLUME` 指令创建的挂载点,无法指定主机上对应的目录,是自动生成的。可以通过`docker volume inspect`查看
```sh
volume ["/data1","/data2"]
```
### EXPOSE-暴露端口
格式为: `EXPOSE <端口1> [<端口2>...]`
`EXPOSE` 指令是**声明容器内运行时提供服务的端口**,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。在 `Dockerfile`中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 `docker run -P` 时,会自动随机映射 `EXPOSE` 的端口(如果`EXPOSE`没有指定端口,那么使用 `-P` 参数无效)。
**真正的暴露端口**是在创建容器`run`的时候指定的` -p <宿主端口>:<容器端口>` 或者 `-P`参数
### ARG-构建参数
构建参数和 `ENV` 的效果一样,都是设置环境变量。所不同的是,`ARG` 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 `ARG` 保存密码之类的信息,因为 `docker history` 还是可以看到所有值的。
`Dockerfile` 中的 `ARG` 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 `docker build` 中用 `--build-arg <参数名>=<值>` 来覆盖。
> ARG指令有生效范围,如果在FROM指令之前指定,那么只能用于FROM指令中。
```dockerfile
ARG DOCKER_USERNAME=library
FROM ${DOCKER_USERNAME}/alpine
RUN set -x ; echo ${DOCKER_USERNAME}
```
>FROM指令之后要使用ARG变量必须再次使用 ARG指令指定
```dockerfile
# 只在FROM中生效
ARG DOCKER_USERNAME=library
FROM ${DOCKER_USERNAME}/alpine
# 要想在FROM之后使用,必须再次指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME}
```
>多阶段构建使用:对于在各个阶段中使用的变量都必须在每个阶段分别指定
```dockerfile
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-指定当前用户
用于指定运行镜像所使用的用户:
```sh
USER user
```
使用`USER`指定用户后,`Dockerfile` 中其后的命令`RUN`、`CMD`、`ENTRYPOINT`都将使用该用户。镜像构建完成后,通过`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`
很多命令如`COPY`、`ADD`等指令中的源文件的路径都是基于**上下文**目录的**相对路径**。
{{< admonition type="info">}}
习惯上我们将`Dockerfile`置于项目根目录,并指定该目录为`docker`构建的**上下文目录**
{{< /admonition >}}
### 多阶段构建方式
每一条`FROM`指令都是一个构建阶段,多条 `FROM`就是多阶段构建
>通过多阶段构建,您可以在`Dockerfile`中使用多个`FROM`语句。每个`FROM`指令可以使用不同的基础,并且每个都开始构建的新阶段。您可以有选择地将前面阶段的构建出的文件复制到后面阶段,从而在最终`image`中留下不需要的所有内容。
例如:
```dockerfile
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`来引用。
```dockerfile
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
```
我们还可以使用 `as` 来为某一阶段命名,例如
```dockerfile
FROM golang:alpine as builder
```
例如当我们只想构建 `builder` 阶段的镜像时,增加 `--target=builder` 参数即可
```
$ docker build --target builder -t username/imagename:tag .
```
**构建时从其他镜像复制文件**
从前面的`builder`阶段构建的镜像中复制文件
```dockerfile
COPY --from=builder /go/src/github.com/go/helloworld/app .
```
也可以复制任意镜像中的文件
```dockerfile
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
```
### 其他构建方式
1.直接用`Git repo`进行构建
```sh
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`压缩包构建
```sh
docker build https://server/context.tar.gz
```
如果所给出的 `URL` 不是个`Git repo`,而是个 `tar` 压缩包,那么`Docker`引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。
{{< admonition type="quote" title="参考文档">}}
[1.Docker - 从入门到实践](https://yeasy.gitbook.io/docker_practice/)
{{< /admonition >}}