Docker镜像拉取、管理、制作操作讲解(命令说明+代码演示)

Docker 运行容器前需要本地存在对应的镜像,如果本地不存在该镜像,Docker 会从镜像仓库下载该镜像。

一、获得镜像

获取镜像的命令是 docker pull。其命令格式为:

docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
  • 具体选项可以使用docker pull --help查看。
  • Docker Registry 地址的格式一般是 [:端口号]。默认地址是 Docker Hub(docker.io)。
  • 仓库名格式是两段式名称,即 /。默认用户名为 library,也就是官方镜像。
  • 默认标签为latest。

当我们下载镜像时,会发现会有多个文件被下载,如

18.04: Pulling from library/ubuntu
92dc2a97ff99: Pull complete
be13a9d27eb8: Pull complete
c8299583700a: Pull complete

这是因为我们之前说过的镜像的分层储存,镜像是由多层存储所构成。下载也是一层层的去下载,并非单一文件。

下载过程中给出了每一层的 ID 的前 12 位。并且下载结束后,给出该镜像完整的 sha256 的摘要,以确保下载一致性。

如果从 Docker Hub 下载镜像非常缓慢,可以添加镜像加速。

在/etc/docker目录中创建daemon.json文件,输入

{
    "registry-mirrors": [
        "https://ung2thfc.mirror.aliyuncs.com",
        "https://registry.docker-cn.com",
        "http://hub-mirror.c.163.com",
        "https://docker.mirrors.ustc.edu.cn"
    ]
}

然后重启docker:

systemctl daemon-reload
systemctl restart docker

二、管理本地镜像

2.1查看镜像

可使用docker image ls命令查看已拉取的镜像。

docker image ls

默认的 docker image ls 列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 -a 参数。(当然可能压根就没有中间层所以根本看不到)

docker image ls -a

可使用docker system df 命令来查看镜像、容器、数据卷所占用的空间。

docker system df 

2.2删除镜像

删除本地的镜像,可以使用 docker image rm 命令:

docker image rm [选项] <镜像1> [<镜像2> ...]

<镜像>可以是镜像短 ID、镜像长 ID、镜像名(:)或者镜像摘要(digest)。

由于一个镜像可以对应多个标签,因此当我们删除了所指定的标签后,可能还有别的标签指向了这个镜像,所以并非所有的 docker image rm 都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已;当该镜像所有的标签都被取消了,该镜像很可能会失去了存在的意义,因此会触发删除行为。

镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变得非常容易,因此很有可能某个其它镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层。

三、制作镜像

(建议:请先完成容器方面的学习后再尝试制作镜像。)

3.1docker commit

注意:docker commit是一种生成镜像的方式,但是这种方式不被推荐。(在容器中进行文件操作时,可能会有大量的无关内容被添加进来,将会导致镜像极为臃肿;另外容器内进行的操作是不会被记录的,生成的镜像被称为黑箱镜像——没人知道执行过什么命令、怎么生成的镜像,进而难以维护。推荐的构建方式是后面要将的dockerfile。)

我将以定制数据库的方式来进行举例。

1、先创建一个mysql容器:sudo docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql。

这个过程,从抽象理解,就是在原先的镜像层上,新建了一个容器存储层,接下来的所有文件修改都会在这个容器存储层中进行。比如我们在数据库中添加了一些数据,这些数据就会被添加到容器存储层中。

如果我们接下来直接关闭容器并删除,这些数据就会消失,因为整个容器存储层都会被删除;但是如果我们使用了commit命令,这个容器存储层就会成为以原先镜像层为基础的另一个镜像层,并且持久化保存下来。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像;以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。

2、生成镜像:docker commit [选项] [[:]]
sudo docker commit ...... mysql:myMysql

3、查看数据是否已被保存

先停止删除原来的容器,然后给新创建的镜像创建个容器。

(运行了一下发现没有,原因是mysql的Dockerfile中有这样一行——VOLUME /var/lib/mysql,导致在容器中的目录/var/lib/mysql的所有修改会对应到宿主机的某个位置...docker commit不会将容器中的/var/lib/mysql目录的更改提交到镜像中;当重新启动commit后的镜像时,container会重新在宿主机中创建一个目录来保存其数据更新,因此并不是原先的宿主机目录,所以新开启的容器看不到之前的数据更改,需要将宿主机对应的目录挂载到新容器的/var/lib/mysql目录下并开启读写权限,然后就可以看到原来的数据了......例子不是很恰当,可以看利用 commit 理解镜像构成 | Docker 从入门到实践 (docker-practice.com),其中举例了一个对nginx文件更改的案例)

3.2docker build&dockerfile

镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层

举例:定制 nginx 镜像

(效果为访问80端口时显示Hello, Docker!)

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

先创建该文件:(输入上述文本)

然后试着启动一下nginx:sudo docker run --name webserver -d -p 80:80 nginx

访问地址后:

然后使用dockerfile脚本构建一层镜像:

docker build -t nginx:myNginx1 .

构建完成,启动容器看看访问后结果:

dockerfile讲解

构建镜像的格式为

docker build [选项] <上下文路径/URL/->
  • FROM:
    • 在 Dockerfile 中,FROM 指令必须是第一条指令,它用于指定基础镜像。基础镜像是在继续构建新镜像时所依赖的镜像,就如举例中的FROM nginx,指定了基础镜像为nginx,对于新建立的镜像来说,nginx镜像是它的最高的中间层镜像。
    • FROM scratch意味着不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
  • RUN:
    • RUN 指令是用来执行命令行命令,有shell和exec两种格式:
      • shell 格式:RUN ,就像直接在命令行中输入的命令一样。如举例中的RUN echo '<h1>Hello, Docker!</h1>'' > /usr/share/nginx/html/index.html
      • exec格式:RUN ["可执行文件", "参数1", "参数2"]。
    • Dockerfile 中每一个指令都会建立一层,并且会有最大层数限制,所以要尽可能减少RUN指令的使用,如果要书写多行命令行,可以这样写(Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以及行首 # 进行注释的格式。):
      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
    • 上下文路径/URL/-:
      • 构建镜像时,经常需要将一些本地文件复制进镜像,通过 COPY 指令、ADD 指令等;但是docker build指令构建镜像时,构建的操作是在服务端进行的,而本地文件在客户端。让服务端获得本地文件,就是需要通过上下文路径。
      • docker build 命令得知上下文路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。(因此如果上下文路径是根目录,会打包发送一大堆无用的文件。)
    • 选项:
      • -t 或者 --tag:用于给构建的新镜像设定一个名字和标签,例如repository:tag. 默认标签是latest。举例中是nginx:myNginx1。
      • -f 或者 --file:指定要用来构建镜像的Dockerfile路径和名字。
      • --build-arg:传递构建参数到Dockerfile中,可以在构建过程中使用这些参数。
      • --pull:总是尝试去获取新版的基础镜像, 即使本地已经存在。
      • --rm:在构建完成后移除中间产生的镜像, 默认是开启的。
      • --no-cache:强制Docker在构建过程中不使用缓存。
其他用法
  1. 从 URL 构建
    1. 如:docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world
  2. 从 tar 压缩包构建
    1. 如:docker build http://server/context.tar.gz
  3. 从标准输入中读取 Dockerfile 进行构建
    1. docker build - < Dockerfile
  4. 从标准输入中读取上下文压缩包进行构建
    1. docker build - < context.tar.gz
    2. 如果发现标准输入的文件格式是 gzip、bzip2 以及 xz 的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。

3.3docker save&docker load

这两个命令本质是用来保存镜像的,docker save用于将镜像保存为文件;docker load用于将文件展开为镜像。

sudo docker save 447125f01362 -o myNginx1_File

-o指定输出文件的文件名。

sudo docker load -i myNginx1_File

-i指定文件来源。

四、Dockerfile指令详解

Dockerfile 功能很强大,它提供了十多个指令。FROM,RUN已经在前文讲解了,这里就不做赘述。

  • COPY:将从构建上下文目录中 的文件/目录复制到新的一层的镜像内的 位置。
    • 格式:COPY [--chown=<user>:<group>] <源路径>... <目标路径>。
    • 举例:COPY package.json /usr/src/app/。
    • 源路径可以是多个,也可以是通配符:COPY hom* /mydir/。
    • 可以加上 --chown=<user>:<group>选项来改变文件的所属用户及所属组。
  • ADD:高级复制文件
    • 格式:ADD <源路径>... <目标路径>
    • 在<源路径> 为一个 tar 压缩文件,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。
    • <源路径>可以是URL,Docker引擎会自动进行下载。
    • 最佳实践:所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD。
  • CMD:(指定docker的主进程)容器启动时默认运行的命令,Dockerfile 中只能有一个 CMD 指令。
    • 与RUN的区别在于,RUN是由于构建镜像的,只有静态文件能被保存下来;CMD是在容器启动时运行的,比如CMD ["nginx", "-g", "daemon off;"],容器启动时会默认执行 nginx -g daemon off; 命令,该命令会以前台方式运行 Nginx。
    • 格式:
      • shell 格式:CMD <命令>
      • exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
      • 参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。
    • 前台与后台:注意,容器只有一个主进程,它将单个进程当作其起始点,即入口点。当启动一个容器时,主进程(PID 1)会运行。容器会跟进该主进程,如果主进程结束,整个容器就会结束。如CMD service nginx start的意思就是CMD [ "sh", "-c", "service nginx start"],执行的主程序是sh,当sh执行完service nginx start后,不会管子进程的nginx,会直接退出,进而导致容器也直接退出;但是如果是直接执行nginx的可执行文件CMD ["nginx", "-g", "daemon off;"],主进程就是nginx,就可以一直运行下去。(当然也有方法衍生子进程,比如使用一种“监控程序”(例如,supervisord)作为主程序,它会对其它进程进行管理。)
  • ENTRYPOINT:入口点,用于指定容器启动程序及参数。
    • 格式:
      • shell 格式:ENTRYPOINT <命令>
      • exec 格式:ENTRYPOINT ["可执行文件", "参数1", "参数2"...]
    • docker run后面可以设置cmd参数;设置ENTRYPOINT后,CMD就会作为参数添加给ENTRYPOINT的可执行文件,就可以在docker run语句后直接添加参数,这些参数会成为可执行文件的参数,如docker run myip -i。(否则,当需要修改参数时,就需要在docker run后面加上完整命令)
      • 也可ENTRYPOINT放一个脚本.sh,使用CMD传入参数。
      • 如:dockerfile是FROM ubuntu:18.04 \n RUN ... \n CMD [ "curl", "-s", "http://myip.ipip.net" ],该容器docker run myip执行后可以输出公网ip。
      • 现在我们想给curl加上-i,就需要docker run myip curl -s http://myip.ipip.net -i,(docker run 后面的额外参数会覆盖掉 CMD 的默认值。)
      • 如果是使用ENTRYPOINT,dockerfile是FROM ubuntu:18.04 \n RUN ... \n ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ],就可以直接docker run myip -i,因为-i会被看做是CMD参数,并成为ENTRYPOINT的参数,然后给curl。(如果不使用ENTRYPOINT,就会被看做是CMD ["-i"],然后去寻找-i的可执行文件)
  • ENV:设置环境变量。
    • 格式:
      • ENV <key> <value>
      • ENV <key1>=<value1> <key2>=<value2>...
    • 如:ENV VERSION=1.0 DEBUG=on NAME="Happy Feet"
    • 可在其他指令中使用$调用。(支持指令:ADD、COPY、ENV、EXPOSE、FROM、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD、RUN。)
  • ARG:构建参数,也是设置环境变量。
    • 格式:ARG <参数名>[=<默认值>]
    • ARG 所设置的环境变量,在将来容器运行时会消失。
    • ARG 指令有生效范围,如果在 FROM 指令之前指定,那么只能用于 FROM 指令中。
    • 可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖ARG,如果Dockerfile中没有定义ARG,则--build-arg不会创建一个新的变量。
  • VOLUME:定义匿名卷
    • 格式:
      • VOLUME ["<路径1>", "<路径2>"...]
      • VOLUME <路径>
    • 如:VOLUME /data
    • 容器运行时需尽量保持容器存储层不发生写操作。这里的 /data 目录就会在容器运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。
  • EXPOSE:声明端口
    • 格式:EXPOSE <端口1> [<端口2>...]。
    • 声明容器运行时提供服务的端口,可以帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另外在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。
    • 注意与-p <宿主端口>:<容器端口>不同,-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
  • WORKDIR:指定工作目录
    • 格式:WORKDIR <工作目录路径>。
    • 使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。
    • 在RUN中进入目录和使用WORKDIR的区别:RUN cd /a && touch 1.txt 是一次性的,只对该命令有效,而 WORKDIR /a 和 RUN touch 1.txt中,WORKDIR /a更改了工作目录,对后续的所有操作都有效,直到路径被另一个WORKDIR命令更改。在实践中,我们通常更倾向于使用WORKDIR,因为这有助于Dockerfile的可读性和维护性。
  • USER:指定当前用户
    • 格式:USER <用户名>[:<用户组>]
    • 和 WORKDIR 相似,WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。
    • USER切换到的用户得事先建立好。
    • 其他切换用户的方式:使用 gosu 运行命令,如使用 myuser 运行 /myapp/myprogram:ENTRYPOINT ["gosu", "myuser", "/myapp/myprogram"]。(与 su 和 sudo 不同,gosu在转换用户和组权限时仍然支持 Docker 中的ENTRYPOINT。)
  • HEALTHCHECK:健康检查
    • 格式:
      • HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令。
      • HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令。
    • 如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了;而Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常,这会导致有部分容器已经无法提供服务了却还在接受用户请求。
    • HEALTHCHECK 用来判断容器主进程的服务状态是否还正常;当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy。
    • HEALTHCHECK 只可以出现一次。
    • 在 HEALTHCHECK [选项] CMD 后面的命令,格式和 ENTRYPOINT 一样,分为 shell 格式,和 exec 格式。命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留,不要使用这个值。
    • 选项:
      • --interval=<间隔>:两次健康检查的间隔,默认为 30 秒;
      • --timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
      • --retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。
    • 举例:
      FROM nginx
      RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
      HEALTHCHECK --interval=5s --timeout=3s \
        CMD curl -fs http://localhost/ || exit 1
    • 健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里,可以用 docker inspect 来查看。
  • LABEL:设置元数据
    • 格式:LABEL <key>=<value> <key>=<value> <key>=<value> ...
    • 给镜像以键值对的形式添加一些元数据(metadata)。可以用来申明镜像的作者、文档地址。
  • SHELL:指定 RUN ENTRYPOINT CMD 指令的 shell
    • 格式:SHELL ["executable", "parameters"]
    • 默认为 ["/bin/sh", "-c"]
  • ONBUILD:构建下一级镜像时执行指令。
    • 格式:ONBUILD 。
    • 指定以当前镜像为基础镜像,构建下一级镜像时会被执行的指令。

 五、多阶段构建

Docker v17.05 开始支持多阶段构建,我们只需要编写一个 Dockerfile即可分离构建阶段和运行阶段。无论构建过程涵盖多少个阶段,多阶段构建最终只会生成一个Docker镜像。这是多阶段构建的主要优点之一,它允许我们在一个Dockerfile中定义多个中间镜像,但最终只保留一个镜像,大大节省了存储空间。

Docker多阶段构建能够有效地减小镜像体积出于以下原因:

分阶段构建:在多阶段构建中,每个阶段可以被看作是一个独立的构建,并且都有自己的基础镜像和中间层。这意味着你可以在第一阶段使用包含大量工具和库的重量级镜像,然后在第二阶段使用较轻量的镜像,且只拷贝第一阶段构建的产物。(理解:不用打包编译的工具,只需要编译的产物

减少了层的数量:每个RUN, COPY, ADD命令都会创建一个新的层。多阶段构建可以通过减少这样的命令从而减少总的层数,再加上一些去除不需要的文件,可以使得最终的镜像体积大大减少。

格式举例:

FROM golang:alpine as builder

RUN apk --no-cache add git

WORKDIR /go/src/github.com/go/helloworld/

RUN go get -d -v github.com/go-sql-driver/mysql

COPY app.go .

RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest as prod

RUN apk --no-cache add ca-certificates

WORKDIR /root/

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

CMD ["./app"]

我们可以使用 as 来为某一阶段命名,例如当我们只想构建 builder 阶段的镜像时,增加 --target=builder 参数即可。

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

我们可以从任何镜像中复制文件,如:COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

举例

官方案例

Docker从入门到实践

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值