Dockerfile学习总结
FROM-指定基础镜像
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。FROM
就是指定基础镜像,一个 Dockerfile
中 FROM
是必备的指令,并且必须是第一条指令。
在Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginx
、redis
、mongo
、mysql
、httpd
、php
、tomcat
等;也有一些方便开发、构建、运行各种语言应用的镜像,如 node
、openjdk
、python
、ruby
、golang
等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch
。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。如果你以 scratch
为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
FROM scratch
...
WORKDIR-指定工作目录
使用 WORKDIR
指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR
会帮你建立目录。
WORKDIR /app
如果你的 WORKDIR
指令使用的相对路径,那么所切换的路径与之前的 WORKDIR
有关:
FROM ubuntu:latest
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
RUN pwd
的工作目录为 /a/b/c
。
RUN-执行命令
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-复制文件
格式: COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY /dir /app
如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径
# error
COPY ../test.txt /app
上述的命令会报错,因为docker
不支持拷贝上下文目录的父目录及其文件 ! ! !
ADD-更高级的复制文件
ADD
比COPY
指令支持更多功能,但是还是建议使用COPY
复制文件,下面给出一个使用ADD
的特例: 自动解压缩如果 <源路径>
为一个 tar
压缩文件的话,压缩格式为 gzip
, bzip2
以及 xz
的情况下,ADD
指令将会自动解压缩这个压缩文件到 <目标路径>
去。
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"...]
。
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环境变量
格式有两种:
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
VOLUME-创建挂载点
下面的命令创建了两个挂载点/data1
和/data2
。通过VOLUME
指令创建的挂载点,无法指定主机上对应的目录,是自动生成的。可以通过docker volume inspect
查看
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指令中。
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
使用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
等指令中的源文件的路径都是基于上下文目录的相对路径。
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
引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。