Docker构建镜像

一、概念

1、基于容器生成镜像

通过 docker commit 命令将现有的容器提交来生成新的镜像。
原理:容器启动后的修改都保存在可写层,通过对可写层的修改生成新的镜像。

[root@hqs docker-hello]# docker commit --help
Usage:  docker commit [OPTIONS选项] CONTAINER容器 [REPOSITORY仓库名[:TAG标签]]
Create a new image from a containers changes
Options:
  -a, --author string    Author (e.g., "John Hannibal Smith <hannibal@a-team.com>")    # 指定作者
  -c, --change list      Apply Dockerfile instruction to the created image             # 允许使用dockerfile的指令
  -m, --message string   Commit message                                  # 提交信息
  -p, --pause            Pause container during commit (default true)    # 创建镜像过程中容器暂停(挂起)
  
 
# 操作流程:运行容器——》修改容器——》容器保存为新的镜像
# 案例:
# 1.运行容器
[hqs-docker@hecs-hqs-01 tester03]$ docker run -ti centos /bin/bash
Unable to find image 'centos:latest' locally
latest: Pulling from library/centos
a1d0c7532777: Pull complete 
Digest: sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177
Status: Downloaded newer image for centos:latest
# 2.修改容器
[root@69b728ed9a9b yum.repos.d]# rm -rf Cent*
[root@69b728ed9a9b yum.repos.d]# vi nginx.repo
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=0
enabled=1
[root@69b728ed9a9b yum.repos.d]# yum install -y nginx
[root@69b728ed9a9b yum.repos.d]# exit
# 3.保存为新的镜像
[hqs-docker@hecs-hqs-01 tester03]$ docker commit 69b728ed9a9b centos-with-nginx
sha256:192cee469ee07eee42d653d65239eff8474d34f359b9431221e341f1e704587d
# 4.启动新容器
[hqs-docker@hecs-hqs-01 tester03]$ docker run -dti centos-with-nginx
0b76bde1cb0f2bbc363541b2f85765d45ae1b0b343ca81af31552fce711fe5b7
[hqs-docker@hecs-hqs-01 tester03]$ docker exec -ti 0b76bde /bin/bash
[root@0b76bde1cb0f /]# find / -name nginx
/usr/share/nginx
/usr/sbin/nginx
/usr/libexec/initscripts/legacy-actions/nginx
/usr/lib64/nginx
/etc/logrotate.d/nginx
/etc/nginx
/var/log/nginx
/var/cache/nginx

基于容器生成镜像无法重复、构建缺乏透明性和体积偏大的问题,因此不推荐使用这种方式构建镜像。

2、Dockerfile 构建镜像

Dockerfile是由一系列指令和参数构成的脚本每一条指令构建一层,因此每一条指令的内容就是描述该层应当如何构建,一个Dockerfile包含了构建镜像的完整指令。

Dockerfile就是一个脚本来构建和定制镜像,把每一层的修改、安装、构建、操作都写入脚本。以此来解决体积、镜像构建透明等问题。

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

3、docker build命令

基于 dockerfile 构建镜像使用 docker build 命令。命令是通过 Dockerfile文件构建上下文(Build Context) 来构建镜像的。
注意:

  1. 不要把多余的文件放到构建上下文中————一般就要创建一个空目录作为构建上下文
  2. 一般将 Dockerfile直接命名为 Dockerfile,并放在上下文根目录,否则构建找不到(可以用-f指定)
  3. Dockerfile的每个指令都被独立执行并创建一个新镜像。
# 语法
[root@localhost docker]# docker build --help
Usage:  docker build [OPTIONS选项] PATH路径 | URL | -
Build an image from a Dockerfile
Options:
      --add-host list           Add a custom host-to-IP mapping (host:ip)                    # 添加映射
      --build-arg list          Set build-time variables                                     # 设置镜像创建时的变量
      --cache-from strings      Images to consider as cache sources
      --cgroup-parent string    Optional parent cgroup for the container
      --compress                Compress the build context using gzip                        # 压缩构建的内容
      --cpu-period int          Limit the CPU CFS (Completely Fair Scheduler) period         # 限制 CPU CFS周期
      --cpu-quota int           Limit the CPU CFS (Completely Fair Scheduler) quota          # 限制 CPU CFS配额
  -c, --cpu-shares int          CPU shares (relative weight)                                 # 设置cpu使用权重
      --cpuset-cpus string      CPUs in which to allow execution (0-3, 0,1)                  # 指定使用的CPU id
      --cpuset-mems string      MEMs in which to allow execution (0-3, 0,1)                  # 指定使用的内存 id
      --disable-content-trust   Skip image verification (default true)                       # 忽略校验,默认开启
  -f, --file string             Name of the Dockerfile (Default is 'PATH/Dockerfile')        # 设置Dockerfile名字
      --force-rm                Always remove intermediate containers                        # 总是删除中间容器
      --iidfile string          Write the image ID to the file                               
      --isolation string        Container isolation technology                               # 使用容器隔离技术
      --label list              Set metadata for an image                                    # 设置镜像使用的元数据
  -m, --memory bytes            Memory limit                                                 # 内存限制
      --memory-swap bytes       Swap limit equal to memory plus swap: '-1' to enable unlimited swap     # 设置Swap的最大值为内存+swap,"-1"表示不限swap
      --network string          Set the networking mode for the RUN instructions during build (default "default")    # 在构建期间设置RUN指令网络模式
      --no-cache                Do not use cache when building the image                     # 构建镜像不使用缓存
      --pull                    Always attempt to pull a newer version of the image          # 总是尝试拉取新版本镜像
  -q, --quiet                   Suppress the build output and print image ID on success      # 安静模式,成功后只打印镜像ID
      --rm                      Remove intermediate containers after a successful build (default true)      # 构建成功后删除中间容器
      --security-opt strings    Security options                                             # 安全选项
      --shm-size bytes          Size of /dev/shm                                             # /dev/shm大小
  -t, --tag list                Name and optionally a tag in the 'name:tag' format           # 镜像的名字及标签
      --target string           Set the target build stage to build.                         # 目标构建阶段设为build
      --ulimit ulimit           Ulimit options (default [])                                  # ?

# 最简案例
docker build .

4、Dockerfile格式

指令:

  1. 指令不区分大小写,建议大写。
  2. 指令可以指定若干参数。
  3. Docker按顺序执行指令。
  4. Dockerfile文件必须以 FROM 指令开头。

注释:

  1. # 号开头的行都将被视为注释。解析器指令除外。
  2. 行中其他位置的 # 视为参数的一部分(不视为注释)。

解析器指令:

  1. 不添加镜像,也不会出现在构建步骤。
  2. 语法:# 指令 = 值,e.g:escape=\
  3. 一旦注释、空行、解析器指令被处理,不再搜寻解析器指令,全格式化为注释。————》解析器指令必须在Dockerfile的首部。

5、.dockerignore文件

可以添加.dockerignore 文件到构建上下文中来定义要排除的文件和目录。
用途:构建镜像时,在将构建上下文传给docker daemon进程时,命令行接口就修改了上下文以排除匹配的文件或目录。有助于避免发送大型文件或敏感文件。

使用要点:

  1. 解释为换行符分隔的模式列表(一行一行读取)
  2. 支持特殊通配符 **,匹配任意数量的目录。

二、Dockerfile的指令

1、FROM指令

FROM指令为后续指令设置基础镜像。

指令写法:

# 不带标签和摘要值,默认选择latest标签的镜像
FROM <镜像>

# 带标签,指定仓库中对应标签版本的镜像
FROM <镜像>:<标签>

# 带摘要值,指定仓库中对应的镜像
FROM <镜像>@<摘要值>

# AS语法指定名称,用处:在后期可以引用此阶段构建的镜像
FROM <镜像> AS <名称>

2、RUN指令

RUN指令在当前镜像顶部创建新的层,执行定义的命令并提交结果。结果产生的镜像由Dockerfile进一步处理。
RUN 指令是用来执行命令行命令的。Run指令在定制镜像时是最常用的指令之一。

注意:

  1. 多少个RUN就构建多少层镜像,多个RUN会造成镜像臃肿,尽量将要执行的命令都写在一个RUN里面。
  2. 多条命令执行可以使用 &&,还可以用 \ 来换行
  3. 执行到最后还需要将不需要的文件和目录删除
  4. Union FS有最大层数限制,如AUFS之前最大不得超过42层,现在是不得超过127层。

(1)shell格式

像直接在命令行输入命令一样。

命令在shell环境中运行:

  • 在Linux系统中默认为 /bin/sh -c命令;
  • 在Windows系统中默认为 cmd /S/C命令。
# 语法格式
RUN <命令>

# 案例
RUN echo '<h1>Hello,Docker!</h1>' > /usr/share/nginx/html/index.html

# 可以用反斜杠将单个RUN指令换行
RUN /bin/bash -c 'source $HOME/.bashrc;\
echo $HOME'

(2)exec格式

更类似函数调用的格式。

# 语法格式
RUN ["可执行文件(程序)", "参数1", "参数2"]

# 案例
RUN ["/bin/bash", "-c", "echo hello"]

3、CMD指令

CMD 指令一般是整个 Dockerfile 的最后一行,主要作用是:为运行中的容器提供默认值。
当Dockerfile 完成所有的环境安装和配置后,CMD指示容器启动时要执行的命令。

注意:Dockerfile中只能有一条CMD命令,如果写了多条则最后一条生效。

(1)exec格式

# 语法
CMD ["可执行文件(程序)", "参数1", "参数2"]

# 案例
CMD ["sh", "-c", "echo $HOME"]
CMD ["/bin/bash"]

(2)ENTRYPOINT指令默认参数

# 语法
CMD ["参数1", "参数2"...]

# 案例??
CMD ["/usr/bin/wc","--help"]

(3)shell格式

# 语法
CMD <命令> 参数1  参数2

# 案例
CMD echo "this is test" | wc -

4、LABEL指令

LABEL 指令向镜像添加标记。即:以键值对的形式给镜像添加一些元数据(metadata)。
其中包含空格,应该使用引号和反斜杠。
一个镜像可以定义多个标记,每个LABEL指令都会产生一层镜像,因此尽量合并在一个LABLE指令里。

# 语法
LABEL <键>=<值> <键>=<值> <键>=<值>...

# 案例
LABEL version="1.0"

# 换行案例
LABEL description="asdasdsad \
标记使用多行"

# 合并标记
LABEL org.opencontainers.image.authors="runoob" version="1.2" description="test"

# 案例
[root@localhost my-centos]# docker inspect --format='{{json .ContainerConfig.Labels}}' my-centos:v2-env
{"com.example.vender":"ACME","datetime":"2023/3/22","description":"声明容器 运行时侦听的网络端口",
"org.label-schema.build-date":"20210915","org.label-schema.license":"GPLv2","org.label-schema.name":"CentOS Base Image",
"org.label-schema.schema-version":"1.0","org.label-schema.vendor":"CentOS",
"version":"1.13"}

5、EXPOSE指令

EXPOSE指令通知(声明)容器在运行时监听指定的网络端口。
可以指定TCP或UDP端口,默认是TCP端口。

注意:EXPOSE指令不会发布端口,只能声明端口,发布端口,要在运行容器时用 -p-P 选项发布。

# 语法
EXPOSE <端口>[<端口>....]

# 案例
EXPOSE 8080
EXPOSE 8080 8081 8082

# 用`-P`暴露端口
[root@localhost my-centos]# docker run -tid -P my-centos:v1-net-plus
[root@localhost my-centos]# docker ps -a
CONTAINER ID   IMAGE                   COMMAND                PORTS                                                NAMES                                                                                                                                      adoring_vaughan
cbc965feaff1   my-centos:v1-net-plus   "/bin/bash"       0.0.0.0:49156->8080/tcp, :::49156->8080/tcp,    pedantic_dijkstra
                            0.0.0.0:49155->8081/tcp, :::49155->8081/tcp, 
                            0.0.0.0:49154->8082/tcp, :::49154->8082/tcp   

6、ENV指令

ENV 指令以键值对的形式定义环境变量。该值会存在于构建镜像阶段后续指令环境中。

# 第一种写法(只支持单个变量设置)
ENV <键> <值>
# 案例:
# 注意空格,第一个空格后的整个字符串都被视为值,因此无法设置多个变量
ENV GOROOT /usr/local/go


# 第二种写法(支持单个、多个变量设置)
ENV <键>=<值> ....
# 单个案例
ENV GOPATH=/Users/hqs/GolangProjects
ENV GOBIN=/Users/hqs/GolangProjects/bin
# 多个案例
ENV GOROOT=/usr/local/go GOPATH=/Users/hqs/GolangProjects \
 GOBIN=/Users/hqs/GolangProjects/bin

# 运行容器检查环境变量
[root@localhost my-centos]# docker run -ti my-centos:v2-env
[root@c308dc947b33 /]# echo $GOROOT
/usr/local/go
[root@c308dc947b33 /]# echo $GOPATH
/Users/hqs/GolangProjects
[root@c308dc947b33 /]# echo $GOBIN 
/Users/hqs/GolangProjects/bin

7、COPY指令

COPY 指令将指定源路径的文件或目录复制到容器文件系统指定的目的路径中。

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

# 选项--chown:改变复制到容器内文件的用户和用户组(默认是UID和GID都为0的用户和组)
# 选项--from=<name|index>:将源位置改为之前构建阶段产生的镜像,可以用名字或数字索引来标识

# <源路径>:源文件或者源目录,这里可以是通配符表达式,其通配符规则要满足 Go 的 filepath.Match 规则。
# <目标路径>:容器内的指定路径,该路径不用事先建好,路径不存在的话,会自动创建。

# 通配符案例:
COPY hom* /mydir/         # 添加所有hom开头文件
COPY hom?.txt /mydir/     # ?替换任意单个字符

# 绝对路径案例:
COPY test /root/absoluteDir/

# 相对路径案例:
COPY test relativeDir/


示例:
# 下载centos8的源
[root@localhost my-centos]# wget http://mirrors.aliyun.com/repo/Centos-8.repo

# Dockerfile文件如下:
FROM centos:latest
LABEL "com.example.vender"="ACME" version="1.13" description="声明容器 \
运行时侦听的网络端口" "datetime"="2023/3/22"

ENV GOROOT=/usr/local/go GOPATH=/Users/hqs/GolangProjects \
 GOBIN=/Users/hqs/GolangProjects/bin

RUN echo 'nameserver 8.8.8.8' > /etc/resolve.conf && rm -rf /etc/yum.repos.d/* && \
  mkdir -p /home/hqs

COPY *.repo /home/
COPY Centos-8.repo /etc/yum.repos.d/

EXPOSE 8080 8081 8082

CMD ["/bin/bash"]

# 构建镜像
[root@localhost my-centos]# docker build -t my-centos:v1-copy .
# 创建容器
[root@localhost my-centos]# docker run -ti cc5b86cb7cb0
[root@7b6c9c6d53f3 /]# yum install -y httpd

COPY指令遵守的复制规则:

  1. 源路径必须位于构建上下文中,不能使用指令COPY ../something/something,因为docker build命令的第1步是发送上下文目录及其子目录到Docker守护进程中。
  2. 如果复制的源是目录,则复制目录的整个内容,包括文件系统元数据。
  3. 如果复制源是任何其他类型的文件,则它会与其元数据被分别复制。在这种情形下,如果目的路径以斜杠(/)结尾,则它将被认为是一个目录,源内容将被写到“<目的>/base(<源>)”路径中。
  4. 如果直接指定多个源,或者源中使用了通配符,则目的路径必须是目录,并且必须以斜杠(/)结尾。
  5. 如果目的路径不以斜杠结尾,则它将被视为常规文件,源内容将被写入目录路径。
  6. 如果目的路径不存在,则会与其路径中所有缺少的目录一起被创建。

8、ADD指令

ADD指令和 COPY指令类似,ADD指令的功能是将主机构建环境(上下文)目录中的文件和目录、或URL标记的文件拷贝到镜像中。

ADD指令和 COPY指令区别:

  1. ADD 指令可以使用URL地址指定。
  2. ADD 指令的归档文件在复制过程中能被自动解压缩。
# 语法
ADD [--chown=<user>:<group>] <源路径1>...  <目标路径>
ADD [--chown=<user>:<group>] ["<源路径1>",...  "<目标路径>"]

# 案例
# ADD指令-三种写法
ADD Centos-8.repo /etc/yum.repos.d/
ADD http://mirrors.aliyun.com/repo/Centos-8.repo /etc/yum.repos.d/
ADD bridge-utils-1.6.tar.xz /home/hqs

# 登录容器后可查看到bridge-utils-1.6.tar.xz已解压
[root@ce2e0b1bf97e hqs]# tree            
.
|-- bridge-utils-1.6
|   |-- AUTHORS
|   |-- COPYING
|   |-- ChangeLog
|   |-- Makefile.in
|   |-- README
|   |-- THANKS
|   |-- TODO
|   |-- brctl
|   |   |-- Makefile.in
|   |   |-- brctl.c

注意:

  1. 源是远程URL地址时,复制产生的文件为600权限(-rw-------),即只有所有者有读写权限,其他用户无法访问。
  2. 源是远程URL地址且不以反斜杠结尾,则下载文件并复制到目的路径。
  3. 源是远程URL地址且以反斜杠结尾,解析出文件名,并下载到"<源>/<文件名>"路径中。
  4. 源是可识别的压缩格式(gzip、bzip2等)的本地tar文件,解压为目录。
  5. 源是远程URL地址,资源不会被解压缩。

9、ENTRYPOINT指令

ENTRYPOINT 指令配置容器的默认入口点,也是配置容器运行的可执行文件。
类似于 CMD 指令,但其不会被 docker run 的命令行参数指定的指令所覆盖,而且这些命令行参数会被当作参数送给 ENTRYPOINT 指令指定的程序。

(1)exec格式

docker run 的命令行参数将附加在exec格式ENTRYPOINT指令后,覆盖CMD指令。

exec格式ENTRYPOINT可以与CMD搭配使用:exec格式的ENTRYPOINT指令设置默认命令和参数,使用CMD来设置更容易修改的其他默认值。

# 语法
ENTRYPOINT ["可执行文件(程序)", "参数1", "参数2"]

# exec格式ENTRYPOINT与CMD搭配使用示例:
FROM centos
RUN rm -rf /etc/yum.repos.d/*
ADD http://mirrors.aliyun.com/repo/Centos-8.repo /etc/yum.repos.d/

# 1.如果容器启动默认是执行某个命令用cmd即可
#CMD ["/bin/bash"]

# 2.如果容器启动默认是执行某个脚本程序则用entrypoint
#ENTRYPOINT ["/usr/local/sbin/test.py"]

# 3.如果执行的脚本可以有参数的话:
#ENTRYPOINT ["/usr/local/sbin/test.py", "-f", "/usr/local/conf/conf.json"]

# 4.如果执行的脚本参数有变换需求的话entrypoint和cmd一起用
ENTRYPOINT ["/usr/local/sbin/test.py", "-f"]
CMD ["/usr/local/conf/conf.json"]


# 1、不传参运行
$ docker run -tid test:v3   # 容器内会默认运行命令,启动主进程。

# 2、传参运行
$ docker run -tid test:v3 /home/hqs/conf.json    # 容器内会默认运行命令,启动主进程

[root@localhost my-centos]# docker ps -a --no-trunc
CONTAINER ID     IMAGE     COMMAND                                              CREATED              STATUS    PORTS     NAMES
7fa2cd3f1f4cc   test:v3   "/usr/local/sbin/test.py -f /home/hqs/conf.json"         2 seconds ago        Created             competent_proskuriakova
bd9b3b310d002   test:v3   "/usr/local/sbin/test.py -f /usr/local/conf/conf.json"   35 seconds ago       Created             suspicious_burnell

docker run 时,可以使用 --entrypoint 选项覆盖ENTRYPOINT指令:

$ docker run -tid --entrypoint /bin/bash/ls  test:v3 -lrt
[root@localhost my-centos]# docker ps -a --no-trunc
CONTAINER ID  IMAGE     COMMAND       CREATED              STATUS                     PORTS     NAMES
5466ed9a4   test:v3   "ls -lrt"      3 seconds ago        Exited (0) 3 seconds ago            wizardly_feistel

(2)shell格式

这种方式会在/bin/sh -c中执行,会忽略任何CMD或者docker run命令行选项。

缺点:ENTRYPOINT指令将作为 /bin/sh -c 的子命令启动,不传任何其他信息,不会接受系统信号,也不会接受docker stop中止信号。

为了确保 docker stop 能够停止长时间运行ENTRYPOINT的容器,推荐使用exec格式。

# 语法
ENTRYPOINT 命令 参数1  参数2

# shell格式ENTRYPOINT示例:
FROM ubuntu
ENTRYPOINT top -b
# 构建镜像
[root@localhost my-ubuntu]# docker build -t test_shell_entry .
Sending build context to Docker daemon   2.56kB
Step 1/2 : FROM ubuntu
latest: Pulling from library/ubuntu
4d32b49e2995: Pull complete 
Digest: sha256:bea6d19168bbfd6af8d77c2cc3c572114eb5d113e6f422573c93cb605a0e2ffb
Status: Downloaded newer image for ubuntu:latest
 ---> ff0fea8310f3
Step 2/2 : ENTRYPOINT top -b
 ---> Running in 09805224b170
Removing intermediate container 09805224b170
 ---> e84ae9ca3b7e
Successfully built e84ae9ca3b7e
Successfully tagged test_shell_entry:latest
# 查看镜像
[root@localhost my-ubuntu]# docker images
REPOSITORY         TAG       IMAGE ID       CREATED          SIZE
test_shell_entry   latest    e84ae9ca3b7e   44 seconds ago   72.8MB
ubuntu             latest    ff0fea8310f3   12 days ago      72.8MB
# 启动容器
[root@localhost my-ubuntu]# docker run -ti --name test test_shell_entry
top - 19:39:21 up  3:27,  0 users,  load average: 0.01, 0.06, 0.06
Tasks:   2 total,   1 running,   1 sleeping,   0 stopped,   0 zombie
%Cpu(s):100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :   1982.6 total,    500.1 free,    646.7 used,    835.7 buff/cache
MiB Swap:   2048.0 total,   2048.0 free,      0.0 used.   1172.9 avail Mem 
# docker stop 等了很久停止容器成功???


# 加入exec命令示例:
[root@localhost my-ubuntu]# cat Dockerfile 
FROM ubuntu
ENTRYPOINT exec top -b
[root@localhost my-ubuntu]# docker build -t top .
[root@localhost my-ubuntu]# docker run -it --name test_exec_entry top
top - 19:45:01 up  3:33,  0 users,  load average: 0.00, 0.02, 0.05
Tasks:   1 total,   1 running,   0 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :   1982.6 total,    498.3 free,    648.3 used,    836.0 buff/cache
MiB Swap:   2048.0 total,   2048.0 free,      0.0 used.   1171.1 avail Mem 
# docker stop 迅速停止容器成功

注意:

  1. 如果 Dockerfile 中如果存在多个 ENTRYPOINT 指令,仅最后一个生效。
  2. 当指定了 ENTRYPOINT 后, CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令。
  3. 要确保 docker stop 能正确终止一直执行的 ENTRYPOINT,需要使用 exec 来启动命令。

10、VOLUME指令

创建一个可以从本地主机或其他容器访问的挂载点。

作用:

  1. 避免重要的数据,因容器重启而丢失,这是非常致命的。(数据保存在主机)
  2. 避免容器不断变大。(数据不在容器,不影响容器大小)
# 语法
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>

# 案例
VOLUME ["/opt/log/"]
# 分别启动两个容器,实现文件共享
[root@localhost my-centos]# docker run -tid -v "$(pwd)"/target:/opt/log/  my-centos:v7-vol
[root@localhost my-centos]# docker run -tid -v "$(pwd)"/target:/app my-centos:v7-vol

# 虽然传递了文件到共享目录,但是容器可写层大小为0
[root@localhost target]# docker ps -s
CONTAINER ID   IMAGE           COMMAND       CREATED         STATUS         PORTS     NAMES                   SIZE
3292f3b32b35   my-centos:vol   "/bin/bash"   6 minutes ago   Up 6 minutes             youthful_banzai         0B (virtual 231MB)
ea772648df98   my-centos:vol   "/bin/bash"   6 minutes ago   Up 6 minutes             focused_johnson         0B (virtual 231MB)

启动容器 docker run 的时候,我们可以通过 -v 参数修改挂载点。

11、WORKDIR指令

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

# 语法
WORKDIR <工作目录路径>

# 案例
WORKDIR /home/hqs
WORKDIR apache
WORKDIR conf
# 登录后路径为:
[root@localhost my-centos]# docker exec -ti 6425fa66b9ec /bin/bash
[root@6425fa66b9ec conf]# pwd
/home/hqs/apache/conf

# 可与搭配ENV指令使用
ENV DIRPATH /path
WORKDIR $DIRPATH/$DIRNAME
# 最终路径则为 /path/$DIRNAME。

注意:

  1. 一个 Dockerfile 可以多次使用 WORKDIR 指令。
  2. 若提供相对路径,该路径将相对于前面的 WORKDIR 指令的路径。
  3. WORKDIR 指令可以在ENV设置变量之后调用环境变量

12、其他指令(USER\ARG\SHELL)

USER:设置运行镜像时使用的用户名(或UID)和可选的用户组(或GID),Dockerfile中的任何RUN、CMD和ENTRYPOINT指令也会使用这个指定的身份。

ARG:定义一个变量,用户可以在使用--build-arg = 标志执行docker build命令构建时将其传递给构建器。

SHELL:用于指定shell格式以覆盖默认的shell。

三、Dockerfile指令使用要点

1、exec和shell格式

RUNCMDENTRYPOINT指令都会用到 exec 和 shell 格式。

一般应该首选 exec 格式,这样的指令可读性更强,更容易理解。RUN 指令两种格式均可。

如使用 CMD 指令为 ENTRYPOINT 指令提供默认参数,则两个指令都应以 JSON 数组格式指定。

(1)exec格式指令直接调用命令,环境变量不会被shell解析

直接调用的案例:

# 创建上下文环境目录和Dockerfile
[hqs-docker@hecs-hqs-01 tester01]$ cat Dockerfile 
FROM ubuntu
ENV name Tester
ENTRYPOINT ["/bin/echo", "Hello,$name"]

# 构建镜像
[hqs-docker@hecs-hqs-01 tester01]$ docker build -t tester01 .

# 运行容器
[hqs-docker@hecs-hqs-01 tester01]$ docker run tester01
Hello,$name       《——————参数中的环境变量没有被shell解析

加入/bin/sh -c 可以让环境变量被shell解析

# 创建上下文环境目录和Dockerfile
[hqs-docker@hecs-hqs-01 ~]$ cp -r tester01/ tester02/

[hqs-docker@hecs-hqs-01 tester02]$ cat Dockerfile 
FROM ubuntu
ENV name Tester02
ENTRYPOINT ["/bin/sh", "-c", "echo Hello,$name"]

# 构建镜像
[hqs-docker@hecs-hqs-01 tester02]$ docker build -t tester02 .

# 运行容器
[hqs-docker@hecs-hqs-01 tester02]$ docker run tester02
Hello,Tester02    《——————参数中的环境变量已经被shell解析

(2)shell格式底层会默认调用/bin/sh -c执行命令

# 创建上下文环境目录和Dockerfile
[hqs-docker@hecs-hqs-01 tester03]$ cat Dockerfile 
FROM ubuntu
ENV name Tester03
ENTRYPOINT echo "Hello,$name!"

# 构建镜像
[hqs-docker@hecs-hqs-01 tester03]$ docker build -t tester03-shell .
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM ubuntu
 ---> ff0fea8310f3
Step 2/3 : ENV name Tester03
 ---> Running in d2b492215377
Removing intermediate container d2b492215377
 ---> 21ca9abdb792
Step 3/3 : ENTRYPOINT echo "Hello,$name!"
 ---> Running in 8793060549fa
Removing intermediate container 8793060549fa
 ---> 98d0dde74246
Successfully built 98d0dde74246
Successfully tagged tester03-shell:latest

# 运行容器
[hqs-docker@hecs-hqs-01 tester03]$ docker run tester03-shell
Hello,Tester03!     《————环境变量已经被shell解析

(3)exec格式没有运行bash和sh的开销

可以在没有bash和sh的镜像中运行,如scratch

# 案例1:
FROM scratch
ADD centos-7-docker.tar.xz /
CMD ["/bin/bash"]

# exec案例:
# 构建上下文和dockerfile
[hqs-docker@hecs-hqs-01 hello01]$ cat Dockerfile 
FROM scratch
COPY hello /
CMD ["/hello"]
# 编译出hello文件
[hqs-docker@hecs-hqs-01 hello01]$ cat hello.c 
#include<stdio.h>
void main (){
    printf("hello docker\n");
}
[hqs-docker@hecs-hqs-01 hello01]$ gcc --static hello.c -o hello
# 构建镜像
[hqs-docker@hecs-hqs-01 hello01]$ docker build -t hello01 .
# 容器运行
[hqs-docker@hecs-hqs-01 hello01]$ docker run hello01
hello docker


# shell案例:
# 构建上下文和dockerfile
[hqs-docker@hecs-hqs-01 ~]$ cp -r hello01/ hello02/
[hqs-docker@hecs-hqs-01 hello02]$ cat Dockerfile 
FROM scratch
COPY hello /
CMD /hello
# 构建镜像
[hqs-docker@hecs-hqs-01 hello02]$ docker build -t hello02 .
# 容器运行
[hqs-docker@hecs-hqs-01 hello02]$ docker run hello02
docker: Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: exec: "/bin/sh": stat /bin/sh: no such file or directory: unknown.

2、RUN、CMD和ENTRYPOINT指令的区别和联系

  1. 执行时间不一样:RUN指令执行命令并创建新的镜像层,RUN先于CMDENTRYPOINT指令在构建镜像时执行;而CMDENTRYPOINT指令在每次启动容器时才执行。
  2. 功能不一样:RUN经常用于安装应用程序和软件包,并被固化在所生成的镜像中。CMD指令的主要目的是为运行容器提供默认值,即默认执行的命令及其参数。
  3. CMDENTRYPOINT执行区别:ENTRYPOINT指令中的参数始终会被 docker run 命令使用,不可改变;而CMD指令中的额外参数可以在执行docker run命令启动容器时被动态替换掉,会被docker run命令所覆盖。
  4. CMDENTRYPOINT一起使用情况:ENTRYPOINT指令作为可执行文件,而CMD指令则为ENTRYPOINT指令提供默认参数。
  5. CMDENTRYPOINT搭配使用要求:如果CMD指令省略可执行文件,必须指定ENTRYPOINT指令;
  6. 必须使用ENTRYPOINT的情况:当容器作为可执行文件时,应该定义ENTRYPOINT指令。

3、组合使用CMD和ENTRYPOINT指令

CMDENTRYPOINT 指令都可以定义运行容器时要执行的命令。组合使用规则:

  1. Dockerfile中应该至少定义一个CMDENTRYPOINT指令。(至少要有一个)
  2. 将整个容器作为一个可执行文件时应当定义ENTRYPOINT指令。(必须定义ENTRYPOINT的情况)
  3. CMD指令应为ENTRYPOINT指令提供默认参数,或者用于容器中执行临时命令。(两个都有的情况)
  4. 当使用替代参数运行容器时,CMD指令的定义将会被覆盖。(docker run时指定参数的情况)

如果CMD指令从基础镜像定义,那么ENTRYPOINT指令的定义会将CMD指令重置为空值。在这种情形下,必须在当前镜像中为CMD指令指定一个实际的值。(不太好理解)

四、Dockerfile构建案例

1、centos-nginx镜像案例

在centos镜像的基础上安装nginx服务器软件构建出新的镜像。

(1)显示nginx首页原本信息

# 1.构建上下文和所需的文件
[root@localhost ~]# mkdir dockerfile-test
[root@localhost ~]# cd dockerfile-test/
[root@localhost dockerfile-test]# touch nginx.repo
[root@localhost dockerfile-test]# touch Dockerfile

# 2.编辑Dockerfile
[root@localhost dockerfile-test]# cat Dockerfile 
# 引入基础镜像
FROM centos
# 设置作者
LABEL maintainer='hqs'
# 清空repo文件
RUN rm -rf /etc/yum.repos.d/*
# 复制nginx.repo
COPY ./nginx.repo /etc/yum.repos.d
# 更新缓存
RUN yum makecache
# 安装nginx
RUN yum install -y nginx
# 改NGINX首页
# 声明对外暴露的端口
EXPOSE 80
# 不以守护进程启动nginx的命令
CMD ["nginx", "-g", "daemon off;"]

# 3.编辑的nginx.repo
[root@localhost dockerfile-test]# cat nginx.repo 
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=0
enabled=1

# 4.镜像构建时网络问题处理(非必须)
# 报错:WARNING: IPv4 forwarding is disabled. Networking will not work.
解决办法:
vi /etc/sysctl.conf
net.ipv4.ip_forward=1         #添加这段代码
# 重启network服务
systemctl restart network
# 查看是否修改成功 (备注:返回1,就是成功)
[root@studyCMachine aaaaaaa]# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

# 5.构建镜像
[root@localhost dockerfile-test]# docker build -t centos-nginx-hqs:1.0 .

# 6.启动容器
[root@localhost dockerfile-test]# docker run --rm -d -p 8000:80 --name my-nginx centos-nginx-hqs:1.0
e5be77c444922817b69acc15810d186634765d27ec0d8ea8e7b345acffc610fa

# 7.访问验证
[root@localhost dockerfile-test]# curl 127.0.0.1:8000
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>

或

在浏览器上访问http://192.168.100.111:8000/

(2)修改nginx首页信息

# 1.复制生成新的上下文环境和相关文件
[root@localhost ~]# cp -r dockerfile-test/ dockerfile-test2/
[root@localhost ~]# cd dockerfile-test2/

# 2.修改Dockerfile文件
[root@localhost dockerfile-test2]# vi Dockerfile 
# 引入基础镜像
FROM centos
# 设置作者
LABEL maintainer='hqs'
# 清空repo文件
RUN rm -rf /etc/yum.repos.d/*
# 复制nginx.repo
COPY ./nginx.repo /etc/yum.repos.d
# 更新缓存
RUN yum makecache
# 安装nginx
RUN yum install -y nginx
# 改NGINX首页
RUN echo "Hello! Please test the nginx server " > /usr/share/nginx/html/index.html 
# 声明对外暴露的端口
EXPOSE 80
# 不以守护进程启动nginx的命令
CMD ["nginx", "-g", "daemon off;"]

# 3.构建镜像
[root@localhost dockerfile-test2]# docker build -t centos-nginx-hqs:2.0 .

# 4.启动容器
[root@localhost dockerfile-test2]# docker run -d -p 8080:80 --name my-nginx-2 centos-nginx-hqs:2.0
749444ad2183c322978c7bf4d58c3f6a1ae617b8f49737d4a79d7ba2b2e76fea

# 5.验证
[root@localhost dockerfile-test2]# curl 127.0.0.1:8080
Hello! Please test the nginx server 
或
浏览器访问:http://192.168.100.111:8080/

2、httpd服务构建

执行以下操作前,先创建上下文环境,拷贝repo文件

mkdir http1.0
cd http1.0
cp /etc/yum.repos.d/CentOS-Base.repo ./local.repo

(1)准备Dockerfile

FROM centos:centos7
LABEL maintainer='zrl'
RUN rm -rf /etc/yum.repos.d/*
COPY ./local.repo /etc/yum.repos.d
RUN yum makecache
RUN yum install -y httpd
EXPOSE 80
CMD ["-D", "FOREGROUND"]
ENTRYPOINT ["/usr/sbin/httpd"]

(2)构建镜像和启动容器

# 构建镜像
[root@localhost http:v1.0]# docker build -t http:v1.0 .

# 启动容器
[root@localhost http:v1.0]# docker run -tid --name web -p 8000:80 http:v1.0 
18121bb92601e6a85d0c53b8bfe08338e5031f75a5e3a36078ae0ca898e89280
[root@localhost http:v1.0]# docker ps
CONTAINER ID   IMAGE       COMMAND                  CREATED         STATUS         PORTS                                   NAMES
18121bb92601   http:v1.0   "/usr/sbin/httpd -D …"   4 seconds ago   Up 3 seconds   0.0.0.0:8000->80/tcp, :::8000->80/tcp   web

# 访问测试
http://10.10.10.111:8000/

3、httpd服务构建进阶

centos7镜像编写dockerfile文件,构建http服务,上传index.html文件到镜像/var/www/html目录,内容为"hello apache"
镜像暴露80端口,且修改参数HOSTNAME为www.example.com,设置httpd服务前台启动,容器将8080端口映射到容器内部80端口。

[root@localhost ~]# mkdir http2.0
[root@localhost yum.repos.d]# cp CentOS-Base.repo /root/http2.0/local.repo

# 编辑index.html文件
[root@localhost http2.0]# vi index.html
hello apache

# 编辑Dockerfile
[root@localhost http2.0]# vi Dockerfile
FROM centos:centos7
LABEL maintainer='zrl'
RUN rm -rf /etc/yum.repos.d/*
COPY ./local.repo /etc/yum.repos.d
RUN yum makecache
RUN yum install -y httpd
COPY ./index.html /var/www/html
EXPOSE 80
CMD ["-D", "FOREGROUND"]
ENTRYPOINT ["/usr/sbin/httpd"]

# 镜像构建
[root@localhost http2.0]# docker build -t http:v2.0 .

# 启动容器
[root@localhost http2.0]# docker run -tid --name http-1 -p 8080:80 -h www.example.com  http:v2.0
784ce9055d90f41b4d5329783d29780d049a31cc0729e07ac5b45a8912d1158f
[root@localhost http2.0]# docker ps 
CONTAINER ID   IMAGE       COMMAND                  CREATED          STATUS          PORTS                                   NAMES
784ce9055d90   http:v2.0   "/usr/sbin/httpd -D …"   15 seconds ago   Up 14 seconds   0.0.0.0:8080->80/tcp, :::8080->80/tcp   http-1

# 查看hostname
[root@localhost http2.0]# docker inspect --format="{{.Config.Hostname}}" http-1
www.example.com

# 访问网站
http://192.168.100.111:8080/

4、本地registry仓库

(1)启动registry私有仓库容器

在本地docker宿主机上加载registry的tar包为registry:latest镜像,并启动为私有仓库容器,将宿主机5000端口映射到容器的5000端口,仓库命名为registry,设置为自启动状态。

# 上传registry_latest.tar包
# 上传完查看包的大小
[root@localhost ~]# du -sh registry_latest.tar 
26M	registry_latest.tar

# 加载镜像
[root@localhost ~]# docker load -i registry_latest.tar 
d9ff549177a9: Loading layer  4.671MB/4.671MB
f641ef7a37ad: Loading layer  1.587MB/1.587MB
d5974ddb5a45: Loading layer  20.08MB/20.08MB
5bbc5831d696: Loading layer  3.584kB/3.584kB
73d61bf022fd: Loading layer  2.048kB/2.048kB
Loaded image: registry:latest
[root@localhost ~]# docker images
REPOSITORY            TAG              IMAGE ID       CREATED          SIZE
registry              latest           f32a97de94e1   3 years ago      25.8MB

# 启动容器
[root@localhost ~]# docker run -d -p 5000:5000 --restart=always --name myregistry -v /opt/data/registry:/var/lib/registry registry
e6384c131de47bdb2720c7a917fb5b1aea040f16f49d9c01c1bea54dc454e184
[root@localhost ~]# docker ps 
CONTAINER ID   IMAGE       COMMAND                  CREATED          STATUS          PORTS                                       NAMES
e6384c131de4   registry    "/entrypoint.sh /etc…"   5 seconds ago    Up 4 seconds    0.0.0.0:5000->5000/tcp, :::5000->5000/tcp   myregistry

(2)上传镜像到本地registry仓库

下载官方的centos:7镜像,将该镜像上传到registry仓库。

# 下载官方的centos:7镜像
[root@localhost ~]# docker pull centos:7
7: Pulling from library/centos
Digest: sha256:9d4bcbbb213dfd745b58be38b13b996ebb5ac315fe75711bd618426a630e0987
Status: Downloaded newer image for centos:7
docker.io/library/centos:7
[root@localhost ~]# docker images
REPOSITORY            TAG              IMAGE ID       CREATED          SIZE
centos                7                eeb6ee3f44bd   9 months ago     204MB

# 给已有的镜像打标签符合自建注册中心格式
[root@localhost ~]# docker tag centos:7 127.0.0.1:5000/centos:7
[root@localhost ~]# docker images
REPOSITORY              TAG              IMAGE ID       CREATED          SIZE
http                    v2.0             0af11e06a37c   33 minutes ago   768MB
127.0.0.1:5000/centos   7                eeb6ee3f44bd   9 months ago     204MB
centos                  7                eeb6ee3f44bd   9 months ago     204MB

# 执行镜像上传
[root@localhost ~]# docker push 127.0.0.1:5000/centos:7
The push refers to repository [127.0.0.1:5000/centos]
174f56854903: Pushed 
7: digest: sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f size: 529

# 执行查看测试
[root@localhost ~]# curl http://127.0.0.1:5000/v2/_catalog
{"repositories":["centos"]}

(3)远程上传镜像到本地registry仓库

在远程docker主机上下载registry的centos:7镜像,将该镜像运行为一个容器,命名为centos-7,在容器安装net-tools工具。
将容器打包封装为新的镜像,命名为centos:net,重新上传到registry。

# 1.克隆一个新的虚拟机(要先把原主机关闭,建议先创建克隆再做其他考试操作)
# 启动两台虚拟机

# 2.修改第二台虚拟机网卡,给一个新的IP,并重启网络
[root@localhost ~]# vi /etc/sysconfig/network-scripts/ifcfg-ens33
IPADDR=192.168.100.118
[root@localhost ~]# systemctl restart network

# 3.第二台主机清理环境(如果需要再做)
[root@localhost ~]# docker rm -f $(docker ps -qa)
[root@localhost ~]# docker rmi 127.0.0.1:5000/centos:7
[root@localhost ~]# docker rmi 192.168.100.111:5000/centos:7
[root@localhost ~]# docker rmi centos:7
[root@localhost ~]# docker images

# 4.检查第一台虚拟机仓库(保证是up状态)
[root@localhost ~]# docker ps -a
CONTAINER ID   IMAGE      COMMAND                  CREATED         STATUS         PORTS                                       NAMES
decca5395c07   registry   "/entrypoint.sh /etc…"   9 minutes ago   Up 3 minutes   0.0.0.0:5000->5000/tcp, :::5000->5000/tcp   myregistry

# 5.第二台主机配置注册中心地址,并重启
[root@localhost ~]# vi /etc/docker/daemon.json 
{
  "registry-mirrors": ["https://nxwgbmaq.mirror.aliyuncs.com"],
  "insecure-registries":["192.168.100.111:5000"]
}
[root@localhost ~]# systemctl restart docker

# 6.第二台主机上拉取远程主机仓库中镜像
[root@localhost ~]# docker pull 192.168.100.111:5000/centos:7
7: Pulling from centos
2d473b07cdd5: Pull complete 
Digest: sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f
Status: Downloaded newer image for 192.168.100.111:5000/centos:7
192.168.100.111:5000/centos:7
[root@localhost ~]# docker images
REPOSITORY                    TAG       IMAGE ID       CREATED        SIZE
192.168.100.111:5000/centos   7         eeb6ee3f44bd   9 months ago   204MB

# 7.第二台主机上拉起新容器
[root@localhost ~]# docker run -tid --name centos-7 192.168.100.111:5000/centos:7 /bin/bash
e22ff1d463a0bf8f8e4de027b9e046d71d34f7726197f15b85d01ec9e801ad0f

# 8.容器中安装net-tools工具
[root@localhost ~]# docker exec -ti centos-7 /bin/bash
[root@e22ff1d463a0 /]# yum install -y  net-tools

# 9.将容器打包封装为新的镜像
[root@localhost ~]# docker commit centos-7 centos:net
sha256:8836665003fa0d465aa3bce1b28951ae60d2bbf6e2bed606e0ef8ac04b275ed8
[root@localhost ~]# docker images
REPOSITORY                    TAG       IMAGE ID       CREATED         SIZE
centos                        net       8836665003fa   6 seconds ago   375MB
192.168.100.111:5000/centos   7         eeb6ee3f44bd   9 months ago    204MB

# 10.重新上传到registry
# 先打标签
[root@localhost ~]# docker tag centos:net  192.168.100.111:5000/centos:net
[root@localhost ~]# docker images
REPOSITORY                    TAG       IMAGE ID       CREATED              SIZE
centos                        net       8836665003fa   About a minute ago   375MB
192.168.100.111:5000/cent

5、ubuntu 运行nginx镜像

(1)创建上下文环境目录和Dockerfile

[root@localhost ~]# mkdir ubuntu-nginx
[root@localhost ubuntu-nginx]# vi Dockerfile
FROM ubuntu
LABEL maintainer='hqs'
ADD sources.list /etc/apt/
RUN apt-get -y update
RUN apt-get install -y nginx
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

(2)创建配置sources.list文件

在编写sources.list文件时,需要先确认当前ubuntu系统版本:

[root@localhost ~]# docker run -ti ubuntu /bin/bash
root@cd939f604f1a:/# cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.3 LTS"

由此得知,当前镜像的系统版本为 20.04

编写sources.list文件如下:

[root@localhost ubuntu-nginx]# vi sources.list
deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse

deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse

deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse

deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse

deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse

(3)构建镜像,启动容器

# 构建镜像
[root@localhost ubuntu-nginx]# docker build -t my-ubuntu/nginx:v1 .
[root@localhost ubuntu-nginx]# docker images
REPOSITORY        TAG       IMAGE ID       CREATED         SIZE
my-ubuntu/nginx   v1        9e93b415f52a   8 minutes ago   201MB

# 启动容器
[root@localhost ubuntu-nginx]# docker run -d -p 8080:80 my-ubuntu/nginx:v1
# 访问网页:http://192.168.200.103:8080/  ,即可访问网站首页。
posted @ 2022-03-30 08:46  休耕  阅读(2478)  评论(0编辑  收藏  举报