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在构建过程中不使用缓存。
- RUN 指令是用来执行命令行命令,有shell和exec两种格式:
其他用法
- 从 URL 构建
- 如:docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world
- 从 tar 压缩包构建
- 如:docker build http://server/context.tar.gz
- 从标准输入中读取 Dockerfile 进行构建
- docker build - < Dockerfile
- 从标准输入中读取上下文压缩包进行构建
- docker build - < context.tar.gz
- 如果发现标准输入的文件格式是 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
举例
更多推荐
所有评论(0)