使用Dockerfile构建镜像

使用Dockerfile构建镜像

本文主要讲解如果通过 Dockerfile 构建自己的镜像。

1、Docker介绍

Docker 可以通过读取 Dockerfile 中的指令自动构建镜像,Dockerfile 是一个文本文档,其中包含了用户创建镜像

的所有命令和说明。

Dockerfile 官方文档:https://docs.docker.com/engine/reference/builder/

Dockerfile 示例:https://github.com/dockerfile/

2、Docker定制镜像

镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都

写入一个脚本,用这个脚本来构建、定制镜像,无法重复的问题、镜像构建透明性的问题、体积的问题就都会解

决,这个脚本就是 Dockerfile。

Dockerfile 是一个文本文件,其内包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容,就是描述

该层应当如何构建。

Docker定制镜像需要我们熟悉 Dockerfile,熟悉每一条构建指令。

3、Dockerfile结构

Dockerfile 结构主要分为四部分:

  • 基础镜像信息
  • 维护者信息
  • 镜像操作指令
  • 容器启动时执行指令 (CMD/ENTRYPOINT)

Dockerfile 每行支持一条指令,每条指令可携带多个参数(支持&&),支持使用以“#“号开头的注释。

Dockerfile 也非必须满足上面的四点。

4、常用Dockerfile操作指令

指令描述
ARG定义创建镜像过程中使用的变量 ,唯一一个可以在 FROM 之前定义。
FROM基础镜像或者称为基于某个镜像,一切从这里开始构建,FROM 前面只能有一个或多个 ARG 指令。
MAINTAINER已弃用,镜像维护者姓名或邮箱地址。
VOLUME镜像挂载的目录,指定容器挂载点到宿主机自动生成的目录或其他容器。
RUN镜像构建的时候需要运行的命令,执行镜像里的命令,跟在 liunx 执行命令一样,只需要在前面加上 RUN 关键词就行。
COPY类似 ADD ,将我们的文件拷贝到镜像中,也就是复制本地(宿主机)上的文件到镜像。
ADD复制并解压(宿主机)上的压缩文件到镜像。
ENV构建的时候设置环境变量。
WORKDIR镜像的工作目录,为 RUN、CMD、ENTRYPOINT、COPY 和 ADD 设置工作目录,就是切换目录 。
EXPOSE保留暴露的端口或者是声明容器的服务端口(仅仅是声明) 。
CMD容器启动后执行的命令 ,多个 CMD 只会执行最后一个,可以被代替。跟 ENTRYPOINT 的区别是,CMD 可以作为 ENTRYPOINT 的参数,且会被 yaml 文件里的 command 覆盖。
ENTRYPOINT容器启动后执行的命令 ,多个只会执行最后一个,可以追加命令。
ONBUILD当构建一个被继承DockerFile 的时候就会运行 ONBUILD 的指令,触发指令。它后面跟的是其它指令,比如 RUN,COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。
HEALTHCHECH健康检查
LABELLABEL 指令用来给镜像添加一些元数据(metadata),以键值对的形式 ,替换 MAINTAINER。
USER为 RUN、CMD、和 ENTRYPOINT 执行命令指定运行用户。
STOPSIGNAL设置将发送到容器退出的系统调用信号。
SHELL覆盖用于命令的 shell 形式的默认 shell。
下面介绍每个指令的使用。

4.1 ARG

定义变量,与 ENV 作用相同,不过 ARG 变量不会像 ENV 变量那样持久化到构建好的镜像中。

构建参数,与 ENV 作用一致。不过作用域不一样。ARG 设置的环境变量仅对 Dockerfile 内有效,也就是说只有

docker build 的过程中有效,构建好的镜像内不存在此环境变量。唯一一个可以在 FROM 之前定义 。构建命令

docker build 中可以用 --build-arg <参数名>=<值> 来覆盖。

# 格式
ARG <参数名>[=<默认值>]
# 示例
# 在FROM之前定义ARG,只在FROM中生效
ARG VERSION=7
FROM centos:${VERSION}
# 在FROM之后使用,得重新定义,不需要赋值
ARG VERSION
RUN echo $VERSION >/tmp/image_version
# 运行
[root@nginx proj]# docker build .
Sending build context to Docker daemon  2.048kB
Step 1/4 : ARG VERSION=7
Step 2/4 : FROM centos:${VERSION}
 ---> eeb6ee3f44bd
Step 3/4 : ARG VERSION
 ---> Using cache
 ---> fc729bc29a1a
Step 4/4 : RUN echo $VERSION >/tmp/image_version
 ---> Using cache
 ---> 20c930478826
Successfully built 20c930478826

Docker 有一组预定义的 ARG 变量,您可以在 Dockerfile 中没有相应指令的情况下使用这些变量。

  • HTTP_PROXY
  • http_proxy
  • HTTPS_PROXY
  • https_proxy
  • FTP_PROXY
  • ftp_proxy
  • NO_PROXY
  • no_proxy

要使用这些,请使用 --build-arg 标志在命令行上传递它们,例如:

$ docker build --build-arg HTTPS_PROXY=https://my-proxy.example.com .

自定义参数传参:

From centos:7
ARG parameter
VOLUME /usr/share/nginx
RUN yum -y install $parameter
EXPOSE 80 443
CMD nginx -g "daemon off;"
# 可以这样灵活传参
docker build --build-arg=parameter=net-tools . 

变量用 $variable_name 或者 ${variable_name} 表示。

  • ${variable:-word} 表示如果 variable 设置,则结果将是该值。如果 variable 未设置,word 则将是结果。
  • ${variable:+word} 表示如果 variable 设置则为 word 结果,否则为空字符串。

变量前加 \ 可以转义成普通字符串:\$foo or \${foo},表示转换为 $foo${foo} 文字。

4.2 FROM

初始化一个新的构建阶段,并设置基础镜像。

定制的镜像都是基于 FROM 的镜像,必选项。

在 Docker Store 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginx、redis、

mongo、mysql、httpd、php、tomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如node、

openjdk、python、ruby、golang等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。

如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如ubuntu、debian、

centos、fedora、alpine 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。

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

实际存在,它表示一个空白的镜像。如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所

写的指令将作为镜像第一层开始存在。使用 Go 语言开发的应用很多会使用这种方式来制作镜像。

# 格式
FROM [--platform=<platform>] <image> [AS <name>]
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
  • 单个 Dockfile 可以多次出现 FROM,以使用之前的构建阶段作为另一个构建阶段的依赖项。

  • AS name 表示为构建阶段命名,在后续 FROM 和 COPY --from=name 说明中可以使用这个名词,引用此阶

    段构建的映像。

  • digest 其实就是就是根据镜像内容产生的一个 ID,只要镜像的内容不变 digest 也不会变。

  • tag 或 digest 值是可选的。如果不使用这两个值时,会使用 latest 版本的基础镜像。。如果找不到该 tag 值,

    构建器将返回错误。

  • –platform 标志可用于在 FROM 引用多平台镜像的情况下指定平台。例如,linux/amd64、linux/arm64、 或

    windows/amd64。默认情况下,使用构建请求的目标平台。全局构建参数可用于此标志的值,例如允许您将

    阶段强制为原生构建平台 ( --platform=$BUILDPLATFORM),并使用它交叉编译到阶段内的目标平台。

# 示例
ARG VERSION=latest
FROM busybox:$VERSION
# FROM --platform="linux/amd64" busybox:$VERSION
ARG VERSION
RUN echo $VERSION > image_version
# 运行
[root@nginx proj]# docker build .
Sending build context to Docker daemon  2.048kB
Step 1/4 : ARG VERSION=latest
Step 2/4 : FROM busybox:$VERSION
 ---> beae173ccac6
Step 3/4 : ARG VERSION
 ---> Using cache
 ---> 4cb57b32b442
Step 4/4 : RUN echo $VERSION > image_version
 ---> Using cache
 ---> 2e954aa2559c
Successfully built 2e954aa2559c

4.3 MAINTAINER

镜像维护者信息,已弃用。

# 格式
MAINTAINER <name>
# 示例
ARG VERSION=latest
FROM busybox:$VERSION
# FROM --platform="linux/amd64" busybox:$VERSION
MAINTAINER zsx242030 zsx242030@qq.com
ARG VERSION
RUN echo $VERSION > image_version
# 运行
[root@nginx proj]# docker build .
Sending build context to Docker daemon  2.048kB
Step 1/4 : ARG VERSION=latest
Step 2/4 : FROM busybox:$VERSION
 ---> beae173ccac6
Step 3/4 : ARG VERSION
 ---> Using cache
 ---> 4cb57b32b442
Step 4/4 : RUN echo $VERSION > image_version
 ---> Using cache
 ---> 2e954aa2559c
Successfully built 2e954aa2559c

MAINTAINER zsx242030 zsx242030@qq.com 这条指令并没有运行。

4.4 RUN

用于执行后面跟着的命令行命令。

将在当前镜像之上的新层中执行命令,在 docker build 时运行。

RUN用于在构建镜像时执行命令,其有以下两种命令执行方式:

# 格式
# shell执行格式
RUN <command>
# exec执行格式
RUN ["executable", "param1", "param2"]
  • 可以使用 \(反斜杠)将单个 RUN 指令延续到下一行。

  • RUN 在下一次构建期间,指令缓存不会自动失效。可以使用 --no-cache 标志使指令缓存无效,如:

    docker build --no-cache。

  • Dockerfile 的指令每执行一次都会在 Docker 上新建一层。所以过多无意义的层,会造成镜像膨胀过大,可以

    使用 && 符号连接命令,这样执行后,只会创建 1 层镜像。

# 示例
# 以下三种写法等价
RUN /bin/bash -c 'source $HOME/.bashrc; \
echo $HOME'

RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'

RUN ["/bin/bash", "-c", "source $HOME/.bashrc; echo $HOME"]
ARG VERSION=7
FROM centos:${VERSION}
# 在FROM之后使用,得重新定义,不需要赋值
ARG VERSION
RUN echo $VERSION >/tmp/image_version
RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'
# 运行
[root@nginx proj]# docker build .
Sending build context to Docker daemon  2.048kB
Step 1/5 : ARG VERSION=7
Step 2/5 : FROM centos:${VERSION}
 ---> eeb6ee3f44bd
Step 3/5 : ARG VERSION
 ---> Using cache
 ---> fc729bc29a1a
Step 4/5 : RUN echo $VERSION >/tmp/image_version
 ---> Using cache
 ---> 20c930478826
Step 5/5 : RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'
 ---> Running in ae598ee2de16
/root
Removing intermediate container ae598ee2de16
 ---> cdeb011e8312
Successfully built cdeb011e8312

每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN 的执行仅仅是当前进程的工

作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容

器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。

4.5 CMD

运行程序,在 docker run 时运行,但是和 run 命令不同,RUN 是在 docker build 时运行。

类似于 RUN 指令,用于运行程序,但二者运行的时间点不同:CMD 在构建镜像时不会执行,在容器运行时运

行。

# 格式
CMD <shell 命令>
CMD ["<可执行文件或命令>","<param1>","<param2>",...]
# 该写法是为ENTRYPOINT指令指定的程序提供默认参数
CMD ["<param1>","<param2>",...]  

推荐使用第二种格式,执行过程比较明确。第一种格式实际上在运行的过程中也会自动转换成第二种格式运行,并

且默认可执行文件是 sh。

指定启动容器时执行的命令,每个 Dockerfile 只能有一条 CMD 命令。如果指定了多条命令,只有最后一条会被执

行。

如果用户启动容器时候指定了运行的命令,则会覆盖掉 CMD 指定的命令。

# 示例
CMD cat /etc/profile
CMD ["/bin/sh","-c","/etc/profile"]
ARG VERSION=7
FROM centos:${VERSION}
# 在FROM之后使用,得重新定义,不需要赋值
ARG VERSION
RUN echo $VERSION >/tmp/image_version
CMD cat /etc/profile
CMD ["/bin/sh","-c","/etc/profile"]
# 运行
[root@nginx proj]# docker build .
Sending build context to Docker daemon  2.048kB
Step 1/6 : ARG VERSION=7
Step 2/6 : FROM centos:${VERSION}
 ---> eeb6ee3f44bd
Step 3/6 : ARG VERSION
 ---> Using cache
 ---> fc729bc29a1a
Step 4/6 : RUN echo $VERSION >/tmp/image_version
 ---> Using cache
 ---> 20c930478826
Step 5/6 : CMD cat /etc/profile
 ---> Running in 4c46debc78ae
Removing intermediate container 4c46debc78ae
 ---> bd512da9e979
Step 6/6 : CMD ["/bin/sh","-c","/etc/profile"]
 ---> Running in 96834f218779
Removing intermediate container 96834f218779
 ---> bdf407b000ec
Successfully built bdf407b000ec

如果希望以后台守护进程形式启动 nginx 服务,name需要使用如下命令:

# 直接执行nginx可执行文件,并且要求以前台形式运行
CMD ["nginx", "-g", "daemon off;"]

4.6 LABEL

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

推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。

# 格式
LABEL <key>=<value> <key>=<value> <key>=<value> ...
# 示例
# 比如我们可以添加镜像的作者
LABEL org.opencontainers.image.authors="SvenDowideit@home.org.au"
LABEL multi.label1="value1" \
      multi.label2="value2" \
      other="value3"
ARG VERSION=7
FROM centos:${VERSION}
# 在FROM之后使用,得重新定义,不需要赋值
ARG VERSION
RUN echo $VERSION >/tmp/image_version
LABEL org.opencontainers.image.authors="SvenDowideit@home.org.au"
LABEL multi.label1="value1" \
      multi.label2="value2" \
      other="value3"
# 运行
[root@nginx proj]# docker build .
Sending build context to Docker daemon  2.048kB
Step 1/6 : ARG VERSION=7
Step 2/6 : FROM centos:${VERSION}
 ---> eeb6ee3f44bd
Step 3/6 : ARG VERSION
 ---> Using cache
 ---> fc729bc29a1a
Step 4/6 : RUN echo $VERSION >/tmp/image_version
 ---> Using cache
 ---> 20c930478826
Step 5/6 : LABEL org.opencontainers.image.authors="SvenDowideit@home.org.au"
 ---> Running in bb21847d492a
Removing intermediate container bb21847d492a
 ---> f06d4cf4412d
Step 6/6 : LABEL multi.label1="value1"       multi.label2="value2"       other="value3"
 ---> Running in 59bc3d46cc11
Removing intermediate container 59bc3d46cc11
 ---> 61913380a529
Successfully built 61913380a529

4.7 EXPOSE

Docker 容器在运行时侦听指定的网络端口。

暴露端口 ,仅仅只是声明端口。

# 格式
# 默认情况下,EXPOSE假定TCP
EXPOSE <port> [<port>/<protocol>...]
  • 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射。

  • 在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

  • 可以指定端口是监听TCP还是UDP,如果不指定协议,默认为TCP。

  • 该 EXPOSE 指令实际上并未发布端口。要在运行容器时实际发布端口,docker run -P 来发布和映射一个或多

    个端口。

  • 默认情况下,EXPOSE 假定 TCP。您还可以指定 UDP:EXPOSE 80/udp。

# 示例
EXPOSE 80/TCP 443/TCP
EXPOSE 80 443
EXPOSE 80/tcp
EXPOSE 80/udp
ARG VERSION=7
FROM centos:${VERSION}
# 在FROM之后使用,得重新定义,不需要赋值
ARG VERSION
RUN echo $VERSION >/tmp/image_version
EXPOSE 80/TCP 443/TCP
# 运行
[root@nginx proj]# docker build .
Sending build context to Docker daemon  2.048kB
Step 1/5 : ARG VERSION=7
Step 2/5 : FROM centos:${VERSION}
 ---> eeb6ee3f44bd
Step 3/5 : ARG VERSION
 ---> Using cache
 ---> fc729bc29a1a
Step 4/5 : RUN echo $VERSION >/tmp/image_version
 ---> Using cache
 ---> 20c930478826
Step 5/5 : EXPOSE 80/TCP 443/TCP
 ---> Running in 566d3bd69e34
Removing intermediate container 566d3bd69e34
 ---> 7e4dc3392336
Successfully built 7e4dc3392336

EXPOSE并不会让容器的端口访问到主机,要使其可访问,需要在docker run运行容器时通过-p来发布这些端口,

或通过-P参数来发布EXPOSE导出的所有端口。

如果没有暴露端口,后期也可以通过 -p 8080:80 方式映射端口,但是不能通过-P形式映射。

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个

端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端

口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射

EXPOSE 的端口。

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话

说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自

动在宿主进行端口映射。

4.8 ENV

设置环境变量,定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。

# 格式
ENV <key1>=<value1> <key2>=<value2>...
# 省略"="此语法不允许在单个ENV指令中设置多个环境变量,并且可能会造成混淆
ENV <key> <value>

设置的环境变量将持续存在,您可以使用 docker inspect 来查看。使用 docker run --env <key>=<value>

更改环境变量的值。

如果环境变量只在构建期间需要,请考虑为单个命令设置一个值:

RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y ...

或者使用 ARG,它不会保留在最终镜像中:

ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y ...
# 示例
ENV JAVA_HOME=/usr/local/jdk
ENV MY_NAME="John Doe" MY_DOG=Rex\ The\ Dog \
    MY_CAT=fluffy
# 此语法不允许在单个ENV指令中设置多个环境变量,并且可能会造成混淆。
ENV JAVA_HOME /usr/local/jdk
ARG VERSION=7
FROM centos:${VERSION}
# 在FROM之后使用,得重新定义,不需要赋值
ARG VERSION
RUN echo $VERSION >/tmp/image_version
ENV JAVA_HOME=/usr/local/jdk
ENV MY_NAME="John Doe" MY_DOG=Rex\ The\ Dog \
    MY_CAT=fluffy
# 此语法不允许在单个ENV指令中设置多个环境变量,并且可能会造成混淆。
ENV JAVA_HOME /usr/local/jdk
# 运行
[root@nginx proj]# docker build .
Sending build context to Docker daemon  2.048kB
Step 1/7 : ARG VERSION=7
Step 2/7 : FROM centos:${VERSION}
 ---> eeb6ee3f44bd
Step 3/7 : ARG VERSION
 ---> Using cache
 ---> fc729bc29a1a
Step 4/7 : RUN echo $VERSION >/tmp/image_version
 ---> Using cache
 ---> 20c930478826
Step 5/7 : ENV JAVA_HOME=/usr/local/jdk
 ---> Running in cc21512fb4a7
Removing intermediate container cc21512fb4a7
 ---> 2161988c0f4c
Step 6/7 : ENV MY_NAME="John Doe" MY_DOG=Rex\ The\ Dog     MY_CAT=fluffy
 ---> Running in 5279fa519a29
Removing intermediate container 5279fa519a29
 ---> 50a4dcfaa261
Step 7/7 : ENV JAVA_HOME /usr/local/jdk
 ---> Running in db273d17689d
Removing intermediate container db273d17689d
 ---> f1f81f6c5ff0
Successfully built f1f81f6c5ff0

4.9 ADD

拷贝文件或目录到容器中,如果是 URL 或压缩包便会自动下载或自动解压 。

复制新文件、目录或远程文件 URL src ,并将它们添加到 dest 中。

src 可以指定多个资源,但如果它们是文件或目录,则它们的路径被解释为相对于构建上下文的源,也就是

WORKDIR。每个都 src 可能包含通配符,匹配将使用 Go 的 filepath.Match 规则。例如:

添加所有以 hom 开头的文件:

ADD hom* /mydir/

在下面的示例中,? 被替换为任何单个字符,例如 home.txt。

ADD hom?.txt /mydir/

dest 是一个绝对路径,或相对 WORKDIR 的相对路径。

ADD 指令和 COPY 的使用格类似(同样需求下,官方推荐使用 COPY)。功能也类似,不同之处如下:

  • ADD 的优点:在执行 <源文件> 为 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,会自动复制

    并解压到 <目标路径>。

  • ADD 的缺点:在不解压的前提下,无法复制 tar 压缩文件。会令镜像构建缓存失效,从而可能会令镜像构建

    变得比较缓慢。具体是否使用,可以根据是否需要自动解压来决定。

# 格式
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
# 示例
# 通配符
ADD hom* /mydir/
# 相对路径,拷贝到WORKDIR目录下relativeDir/
ADD test.txt relativeDir/
# 绝对路径
ADD test.txt /absoluteDir/
# 更改权限
ADD --chown=55:mygroup files* /somedir/
ADD --chown=bin files* /somedir/
ADD --chown=1 files* /somedir/
ADD --chown=10:11 files* /somedir/

ADD 和 COPY 的区别和使用场景:

  • ADD 支持添加远程 url 和自动提取压缩格式的文件,COPY 只允许从本机中复制文件

  • COPY 支持从其他构建阶段中复制源文件(–from)

  • 根据官方 Dockerfile 最佳实践,除非真的需要从远程 url 添加文件或自动提取压缩文件才用 ADD,其他情况

    一律使用 COPY

4.10 COPY

语法同ADD一致,复制拷贝文件。

COPY 指令和 ADD 指令的唯一区别在于:是否支持从远程URL获取资源。COPY 指令只能从执行 docker build 所

在的主机上读取资源并复制到镜像中。而 ADD 指令还支持通过 URL 从远程服务器读取资源并复制到镜像中。

相同需求时,推荐使用 COPY 指令,ADD 指令更擅长读取本地tar文件并解压缩。

拷贝(宿主机)文件或目录到容器中,跟 ADD 类似,但不具备自动下载或解压的功能 。所有新文件和目录都使用 0

的 UID 和 GID 创建,除非可选 --chown 标志指定给定的用户名、组名或 UID/GID 组合以请求复制内容的特定所

有权。

# 格式
# 源路径可以是多个,甚至可以是通配符,其通配符规则要满足Go的filepath.Match规则
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
# 示例
# 添加所有以hom开头的文件
COPY hom* /mydir/
# ?替换为任何单个字符,例如home.txt
COPY hom?.txt /mydir/
# 使用相对路径,并将test.txt添加到<WORKDIR>/relativeDir/
COPY test.txt relativeDir/
# 使用绝对路径,并将test.txt添加到/absoluteDir/
COPY test.txt /absoluteDir/

# 修改文件权限
COPY --chown=55:mygroup files* /somedir/
COPY --chown=bin files* /somedir/
COPY --chown=1 files* /somedir/
COPY --chown=10:11 files* /somedir/

4.11 ENTRYPOINT

类似于 CMD 指令,但其不会被 docker run 的命令行参数指定的指令所覆盖,而且这些命令行参数会被当作参数

送给 ENTRYPOINT 指令指定的程序。但是,如果运行 docker run 时使用了 --entrypoint 选项,将覆盖

ENTRYPOINT 指令指定的程序。在 k8s 中 command 也会覆盖 ENTRYPOINT 指令指定的程序。

它有2种格式:

# exec形式,这是首选形式
ENTRYPOINT ["executable", "param1", "param2"]
# shell形式
ENTRYPOINT command param1 param2

指定了 ENTRYPOINT 后, CMD 的内容作为参数传给 ENTRYPOINT 指令,实际执行时,将变为:

<ENTRYPOINT> <CMD>

示例:

FROM ubuntu
ENTRYPOINT ["top", "-b"]
# CMD作为ENTRYPOINT参数
CMD ["-c"]
# 与下面的等价
ENTRYPOINT ["top", "-b -c"]
ENTRYPOINT  top -b -c

docker run 传递的参数,都会先覆盖 cmd,然后由cmd 传递给entrypoint。

注意:如果 Dockerfile 中如果存在多个 ENTRYPOINT 指令,仅最后一个生效。

ARG VERSION=7
FROM centos:${VERSION}
# 在FROM之后使用,得重新定义,不需要赋值
ARG VERSION
RUN echo $VERSION >/tmp/image_version
ENTRYPOINT ["top", "-b"]
# CMD作为ENTRYPOINT参数
CMD ["-c"]
# 运行
[root@nginx proj]# docker build .
Sending build context to Docker daemon  2.048kB
Step 1/6 : ARG VERSION=7
Step 2/6 : FROM centos:${VERSION}
 ---> eeb6ee3f44bd
Step 3/6 : ARG VERSION
 ---> Using cache
 ---> fc729bc29a1a
Step 4/6 : RUN echo $VERSION >/tmp/image_version
 ---> Using cache
 ---> 20c930478826
Step 5/6 : ENTRYPOINT ["top", "-b"]
 ---> Running in d722c6faafd6
Removing intermediate container d722c6faafd6
 ---> 7e1ef1220173
Step 6/6 : CMD ["-c"]
 ---> Running in 449c6ca4eed2
Removing intermediate container 449c6ca4eed2
 ---> fcb93226d375
Successfully built fcb93226d375

注:ENTRYPOINT 与 CMD非常类似,不同的是通过 docker run 执行的命令不会覆盖 ENTRYPOINT,而 docker

run 命令中指定的任何参数,都会被当做参数再次传递给CMD。

Dockerfile中只允许有一个ENTRYPOINT命令,多指定时会覆盖前面的设置,而只执行最后的ENTRYPOINT指令。

通常情况下,ENTRYPOINT 与CMD一起使用,ENTRYPOINT 写默认命令,当需要参数时候使用CMD传参。

4.12 VOLUME

创建一个具有指定名称的挂载数据卷,也就是指定此目录可以被挂载出去。

定义匿名数据卷,在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。

作用:

  • 避免重要的数据,因容器重启而丢失,这是非常致命的。
  • 避免容器不断变大。
  • 在启动容器 docker run 的时候,我们可以通过 -v 参数修改挂载点。
# 格式
# 后面路径是容器内的路径,对应宿主机的目录是随机的
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
# 示例
FROM ubuntu
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol
ARG VERSION=7
FROM centos:${VERSION}
# 在FROM之后使用,得重新定义,不需要赋值
ARG VERSION
RUN echo $VERSION >/tmp/image_version
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol
# 运行
[root@nginx proj]# docker build .
Sending build context to Docker daemon  2.048kB
Step 1/7 : ARG VERSION=7
Step 2/7 : FROM centos:${VERSION}
 ---> eeb6ee3f44bd
Step 3/7 : ARG VERSION
 ---> Using cache
 ---> fc729bc29a1a
Step 4/7 : RUN echo $VERSION >/tmp/image_version
 ---> Using cache
 ---> 20c930478826
Step 5/7 : RUN mkdir /myvol
 ---> Running in 0ae91dea652f
Removing intermediate container 0ae91dea652f
 ---> 3659e073c718
Step 6/7 : RUN echo "hello world" > /myvol/greeting
 ---> Running in adb34f510b69
Removing intermediate container adb34f510b69
 ---> 122170713ebc
Step 7/7 : VOLUME /myvol
 ---> Running in 5980e67a8495
Removing intermediate container 5980e67a8495
 ---> 52a7f27f5ac6
Successfully built 52a7f27f5ac6

注:一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:

  • 卷可以容器间共享和重用
  • 容器并不一定要和其它容器共享卷
  • 修改卷后会立即生效
  • 对卷的修改不会对镜像产生影响
  • 卷会一直存在,直到没有任何容器在使用它

4.13 WORKDIR

指定工作目录,用 WORKDIR 指定的工作目录,会在构建镜像的每一层中都存在。(WORKDIR 指定的工作目录,

必须是提前创建好的)。

工作目录,如果 WORKDIR 不存在,即使它没有在后续 Dockerfile 指令中使用,它也会被创建。

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

docker build 构建镜像过程中,每一个 RUN 命令都会新建一层。只有通过 WORKDIR 创建的目录才会一直存在。

可以设置多个 WORKDIR,如果提供了相对路径,它将相对于前一条 WORKDIR 指令的路径。例如:

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

最终 pwd 命令的输出是 /a/b/c。

该 WORKDIR 指令可以解析先前使用 ENV,例如:

ENV DIRPATH=/path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd

最终 pwd 命令的输出是 /path/$DIRNAME。

# 示例
FROM busybox
ENV FOO=/bar
# WORKDIR /bar
WORKDIR ${FOO}   
# 运行
[root@nginx proj]# docker build .
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM busybox
 ---> beae173ccac6
Step 2/3 : ENV FOO=/bar
 ---> Using cache
 ---> 5d417a922dcc
Step 3/3 : WORKDIR ${FOO}
 ---> Using cache
 ---> bf23e9829e30
Successfully built bf23e9829e30

通过WORKDIR设置工作目录后,Dockerfile中其后的命令RUN、CMD、ENTRYPOINT、ADD、COPY 等命令都会

在该目录下执行。在使用docker run运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。

4.14 USER

用于指定执行后续命令的用户和用户组,这边只是切换后续命令执行的用户(用户和用户组必须提前已经存在)。

设置用户名(或 UID )和可选的用户组(或 GID)。

使用USER指定用户时,可以使用用户名、用户组、UID或GID,或是两者的组合。

使用USER指定用户后,Dockerfile中其后的命令RUN、CMD、ENTRYPOINT都将使用该用户。

# 格式
USER <用户名>[:<用户组>]
USER <UID>[:<GID>]
# 示例
FROM centos:7
RUN groupadd --system --gid=9999 xiaoming && useradd --system --home-dir /home/xiaoming --uid=9999 --gid=xiaoming xiaoming
USER xiaoming:xiaoming
# USER 9999:9999
# 运行
[root@nginx proj]# docker build .
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM centos:7
 ---> eeb6ee3f44bd
Step 2/3 : RUN groupadd --system --gid=9999 xiaoming && useradd --system --home-dir /home/xiaoming --uid=9999 --gid=xiaoming xiaoming
 ---> Running in a2ee724408fe
Removing intermediate container a2ee724408fe
 ---> e1a7cae6f745
Step 3/3 : USER xiaoming:xiaoming
 ---> Running in 01432294ca93
Removing intermediate container 01432294ca93
 ---> 85b0009ed8d6
Successfully built 85b0009ed8d6

4.15 ONBUILD

ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN,COPY 等,而这些指令,在当前镜像构建时并

不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。

将一个触发指令添加到镜像中,以便稍后在该镜像用作另一个构建的基础时执行。也就是另外一个 dockerfile

FROM 了这个镜像的时候执行。

# 格式
ONBUILD <其它指令>
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
# 示例
FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]
# 运行
[root@nginx proj]# docker build .
Sending build context to Docker daemon  2.048kB
Step 1/7 : FROM node:slim
 ---> a6a0d486ccb2
Step 2/7 : RUN mkdir /app
 ---> Using cache
 ---> 66fae2f17ec8
Step 3/7 : WORKDIR /app
 ---> Using cache
 ---> 32ae662043ac
Step 4/7 : ONBUILD COPY ./package.json /app
 ---> Using cache
 ---> 56519d4a4583
Step 5/7 : ONBUILD RUN [ "npm", "install" ]
 ---> Using cache
 ---> 295afb3288cc
Step 6/7 : ONBUILD COPY . /app/
 ---> Using cache
 ---> c2ea4679df0f
Step 7/7 : CMD [ "npm", "start" ]
 ---> Using cache
 ---> adb8fd0d7de1
Successfully built adb8fd0d7de1

4.16 HEALTHCHECK

用于指定某个程序或者指令来监控 docker 容器服务的运行状态。

该 HEALTHCHECK 指令有两种形式:

# 格式
# 通过在容器内运行命令检查容器运行状况
HEALTHCHECK [OPTIONS] CMD command
# 禁用从基础映像继承的任何运行状况检查
HEALTHCHECK NONE

选项 CMD 有:

  • --interval=DURATION(默认30s:):间隔,频率

  • --timeout=DURATION(默认30s:):超时时间

  • --start-period=DURATION(默认0s:):为需要时间引导的容器提供初始化时间, 在此期间探测失败将

    不计入最大重试次数。

  • --retries=N(默认3:):重试次数

命令的 exit status 指示容器的运行状况。可能的值为:

  • 0:健康状态,容器健康且已准备完成。
  • 1:不健康状态,容器工作不正常。
  • 2:保留,不要使用此退出代码。
# 示例
FROM nginx
HEALTHCHECK --interval=5s --timeout=3s \
  CMD curl -f http://localhost/ || exit 1
CMD ["usr/sbin/nginx", "-g", "daemon off;"]
# 运行
[root@nginx proj]# docker build .
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM nginx
 ---> 605c77e624dd
Step 2/3 : HEALTHCHECK --interval=5s --timeout=3s   CMD curl -f http://localhost/ || exit 1
 ---> Using cache
 ---> 4b5068464b9d
Step 3/3 : CMD ["usr/sbin/nginx", "-g", "daemon off;"]
 ---> Using cache
 ---> 694713b076e4
Successfully built 694713b076e4

4.17 STOPSIGNAL

设置将发送到容器退出的系统调用信号。该信号可以是与内核系统调用表中的位置匹配的有效无符号数,例如 9,

或格式为 SIGNAME 的信号名称,例如 SIGKILL。

# 格式
STOPSIGNAL signal

默认的 stop-signal 是 SIGTERM,在 docker stop 的时候会给容器内 PID 为 1 的进程发送这个 signal,通过

–stop-signal 可以设置自己需要的 signal,主要目的是为了让容器内的应用程序在接收到 signal 之后可以先处理

一些事物,实现容器的平滑退出,如果不做任何处理,容器将在一段时间之后强制退出,会造成业务的强制中断,

默认时间是 10s。

4.18 SHELL

覆盖用于命令的 shell 形式的默认 shell。

Linux 上的默认 shell 是 [“/bin/sh”, “-c”],Windows 上是 [“cmd”, “/S”, “/C”]。

# 格式
SHELL ["executable", "parameters"]

该 SHELL 指令在 Windows 上特别有用,因为 Windows 有两种常用且截然不同的本机 SHELL:cmd 和

powershell,以及可用的备用 shell,包括 sh。该 SHELL 指令可以出现多次。每条 SHELL 指令都会覆盖所有先前

的 SHELL 指令,并影响所有后续指令。

5、ARG 和 ENV 的区别

  • ARG 定义的变量只会存在于镜像构建过程,启动容器后并不保留这些变量
  • ENV 定义的变量在启动容器后仍然保留

6、镜像构建(docker build)

docker build 命令用于使用 dockerfile 创建镜像。

在完成 dockerfile 文件的编写后,执行 docker build 命令,则会根据 dockerfile 文件中上下文的内容构建新的

docker 镜像。

整个构建过程会被递归处理,如果在 dockerfile 中包含了路径或者URL,都会被递归构建。

# 语法
# docker build [选项] <上下文路径/URL/->
$ docker build [OPTIONS] PATH | URL | -
# 直接用 Git repo 进行构建
$ docker build https://github.com/twang2218/gitlab-ce-zh.git#:8.14
# 用给定的 tar 压缩包构建
$ docker build http://server/context.tar.gz
# 从标准输入中读取 Dockerfile 进行构建
$ docker build - < Dockerfile
# 或
$ cat Dockerfile | docker build -
# 从标准输入中读取上下文压缩包进行构建
$ docker build - < context.tar.gz

常用参数:

参数解释
–build-arg=[]设置镜像创建时的变量;
–cpu-shares设置 cpu 使用权重;
–cpu-period限制 CPU CFS周期;
–cpu-quota限制 CPU CFS配额;
–cpuset-cpus指定使用的CPU id;
–cpuset-mems指定使用的内存 id;
–disable-content-trust忽略校验,默认开启;
-f指定要使用的Dockerfile路径;
–force-rm设置镜像过程中删除中间容器;
–isolation使用容器隔离技术;
–label=[]设置镜像使用的元数据;
-m设置内存最大值;
–memory-swap设置Swap的最大值为内存+swap,"-1"表示不限swap;
–no-cache创建镜像的过程不使用缓存;
–pull尝试去更新镜像的新版本;
–quiet, -q安静模式,成功后只输出镜像 ID;
–rm设置镜像成功后删除中间容器;
–shm-size设置/dev/shm的大小,默认值是64M;
–ulimitUlimit配置。
–squash将 Dockerfile 中所有的操作压缩为一层。
–tag, -t:镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签。
–network:默认 default。在构建期间设置RUN指令的网络模式
# 例如
$ docker build -t text:v1 . --no-cache

# 要在构建后将映像标记到多个存储库中,请在运行命令-t时添加多个参数
$ docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest .

# 参数解释
# -t: 指定镜像名称
# .: 当前目录Dockerfile
# -f: 指定Dockerfile路径
# --no-cache: 不缓存

7、运行容器测试(docker run)

# 非交互式运行
$ docker run centos:7.4.1708 /bin/echo "Hello world"

# 交互式执行
# -t: 在新容器内指定一个伪终端或终端
# -i: 允许你对容器内的标准输入(STDIN)进行交互
# 会登录到docker环境中,交互式
$ docker run -it centos:7.4.1708 /bin/bash

# -d: 后台执行,加了-d参数默认不会进入容器
$ docker run -itd centos:7.4.1708 /bin/bash

# 进入容器
# 在使用-d参数时,容器启动后会进入后台,此时想要进入容器,可以通过以下指令进入
# docker exec -it: 推荐大家使用docker exec -it命令,因为此命令会退出容器终端,但不会导致容器的停止
# docker attach: 容器退出,会导致容器的停止
$ docker exec -it  b2c0235dc53 /bin/bash
$ docker attach  b2c0235dc53

举例,我们先生成一个测试镜像:

ARG VERSION=7
FROM centos:${VERSION}
ARG VERSION
RUN echo $VERSION >/tmp/image_version
[root@nginx proj]# docker build -t centos:v1 . --no-cache
Sending build context to Docker daemon  2.048kB
Step 1/4 : ARG VERSION=7
Step 2/4 : FROM centos:${VERSION}
 ---> eeb6ee3f44bd
Step 3/4 : ARG VERSION
 ---> Running in a972227c4f45
Removing intermediate container a972227c4f45
 ---> 0d9282811614
Step 4/4 : RUN echo $VERSION >/tmp/image_version
 ---> Running in 3a0216156845
Removing intermediate container 3a0216156845
 ---> 8892bf29e6f2
Successfully built 8892bf29e6f2
Successfully tagged centos:v1
# 非交互式运行
[root@nginx proj]# docker run centos:v1 /bin/echo "Hello world"
Hello world
# 交互式执行
[root@nginx proj]# docker run -it centos:v1 /bin/bash
[root@1690a3ff1010 /]# echo "Hello world"
Hello world
[root@1690a3ff1010 /]# exit
exit
# 后台启动
[root@nginx proj]# docker run -itd centos:v1 /bin/bash
dbc0f377ebb507866fd1051862bebbdc5a14121724b76fc85606df99ab7681ae
[root@nginx proj]# docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                            NAMES
dbc0f377ebb5   centos:v1      "/bin/bash"              4 seconds ago   Up 2 seconds                                    laughing_herschel
# 进入容器
[root@nginx proj]# docker exec -it  dbc0f377ebb5 /bin/bash
[root@dbc0f377ebb5 /]# echo "Hello world"
Hello world
[root@dbc0f377ebb5 /]# exit
exit

8、制作镜像

如果有多个RUN,自上而下依次运行,每次运行都会形成新的层,建议 && 放入一行运行。

如果有多个CMD,只有最后一个运行。

如果有多个Entrypoint,只有最后一个运行。

如果CMD和entrypoint共存,只有entrypoint运行,且最后的CMD会当做entrypoint的参数。

镜像制作分为两个阶段:

  • docker build 阶段,基于dockerfile制作镜像 (RUN,用于此阶段的运行命令)

  • docker run 阶段,基于镜像运行容器 (CMD,基于image run容器时候,需要运行的命令)

  • docker build 基于第一阶段的镜像被别人from制作新镜像 (entrypoint 或onbuild 基于镜像重新构建新镜像

    时候在此阶段运行的命令)

一个简单的centos镜像:

# 指定基础镜像为centos
FROM centos:7
MAINTAINER zsx242030
ENV MYPATH /usr/local
WORKDIR $MYPATH
RUN yum -y install vim
RUN yum -y install net-tools
CMD echo $MYPATH
# 生成镜像
[root@nginx proj]# docker build -t mycentos:v1 .
# 查看镜像
[root@nginx proj]# docker images | grep mycentos
mycentos     v1        fb4525b73891   9 seconds ago   677MB
# 测试
[root@nginx proj]# docker run mycentos:v1 ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.4  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:ac:11:00:04  txqueuelen 0  (Ethernet)
        RX packets 1  bytes 90 (90.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
  
# 没有安装wget会报错
[root@nginx proj]# docker run mycentos:v1 wget
docker: Error response from daemon: OCI runtime create failed: runc create failed: unable to start container process: exec: "wget": executable file not found in $PATH: unknown.
ERRO[0000] error waiting for container: context canceled

9、ENTRYPOINT CMD的好处

那么有了 CMD 后,为什么还要有 ENTRYPOINT 呢?这种 <ENTRYPOINT> <CMD> 有什么好处么?让我们来看几个

场景。

9.1 让镜像变成像命令一样使用

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

FROM centos:7
CMD [ "curl", "-s", "http://ip.cn" ]

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

$ docker run myip

这么看起来好像可以直接把镜像当做命令使用了。

如果我们希望显示 HTTP 头信息,就需要加上 -i 参数。

$ docker run 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,而不是添加在原来的 curl -s http://ip.cn 后面。而 -i 根本不是命令,所

以自然找不到。

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

$ docker run myip curl -s http://ip.cn -i

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

个镜像:

FROM ubuntu:16.04
ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]

这次我们就可以直接使用 docker run myip -i

9.2 应用运行前的准备工作

启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。

比如 mysql 类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前

解决。

此外,可能希望避免使用 root 用户去启动服务,从而提高安全性,而在启动服务前还需要以 root 身份执行一些必

要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 root 身份执行,方

便调试等。

这些准备工作是和容器 CMD 无关的,无论 CMD 为什么,都需要事先进行一个预处理的工作。这种情况下,可以

写一个脚本,然后放入 ENTRYPOINT 中去执行,而这个脚本会将接到的参数(也就是 CMD 作为命令,在脚本最

后执行。

...
ENTRYPOINT ["init.sh"]
......
EXPOSE 6379
CMD [ "default-user" ]

10、CMD,ENTRYPOINT,command,args缺省说明

当用户同时在 kubernetes 中的 yaml 文件中写了 command 和 args 的时候,默认是会覆盖 DockerFile 中的命令

行和参数,完整的情况分类如下:

1、command 和 args 不存在

如果 command 和 args 都没有写,那么用 DockerFile 默认的配置。

2、command 存在,但 args 不存在

如果 command 写了,但 args 没有写,那么 Docker 默认的配置会被忽略而且仅仅执行 yaml 文件的

command(不带任何参数的)。

3、command 不存在,但 args 存在

如果 command 没写,但 args 写了,那么 Docker 默认配置的 ENTRYPOINT 的命令行会被执行,但是调用的参

数是 yaml 中的 args,CMD 的参数会被覆盖,但是 ENTRYPOINT 自带的参数还是会执行的。

4、command 和 args 都存在

如果如果 command 和 args 都写了,那么 Docker 默认的配置被忽略,使用 yaml 的配置。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值