获取镜像

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



docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]





docker exec后面的参数 docker exec_docker exec -it


Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号]。默认地址是 Docker Hub。

仓库名:如之前所说,这里的仓库名是两段式名称,即 <用户名>/<软件名>。

对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。

##运行


docker run -it --rm redis bash


  • -it:这是两个参数
  • 一个是 -i:交互式操作
  • 一个是 -t 终端。
  • 这里打算进入 bash 执行一些命令并查看返回结果,因此我们需要交互式终端。
  • --rm:这个参数是说容器退出后随之将其删除。

##列出镜像 docker image ls


docker exec后面的参数 docker exec_docker exec -it_02


列表包含了 仓库名、标签、镜像 ID、创建时间 以及 所占用的空间

###镜像体积 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。而 docker image ls 显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。

docker image ls 列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。 你可以通过以下命令来便捷的查看镜像、容器、数据卷所占用的空间。


docker system df


docker exec后面的参数 docker exec_docker 命令_03


###虚悬镜像(dangling image) 显示虚悬镜像:


docker image ls -f dangling=true


docker exec后面的参数 docker exec_docker run 参数_04


清除虚悬镜像:


docker image prune


docker exec后面的参数 docker exec_docker exec后面的参数_05


镜像既没有仓库名,也没有标签,均为 “<none>”

导致虚悬像原因:

  • docker pull 镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消
  • docker build 由于新旧镜像同名,旧镜像名称被取消

###中间层镜像 为了加速镜像构建、重复利用资源,Docker 会利用 中间层镜像。所以在使用一段时间后,可能会看到一些依赖的中间层镜像。默认的 docker image ls 列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 -a 参数。


显示顶层镜像
docker image ls 
显示顶层镜像及中间层镜像
docker image ls -a


这样会看到很多无标签的镜像,与之前的虚悬镜像不同,这些无标签的镜像很多都是中间层镜像,是其它镜像所依赖的镜像。这些无标签镜像不应该删除,否则会导致上层镜像因为依赖丢失而出错。

列出部分镜像


docker image ls ubuntu:18.04
docker image ls -f since=mongo:3.2
docker image ls -f before=mongo:3.2


还可以通过 LABEL 来过滤。


docker image ls -f label=com.andrick.version=0.1


###以特定格式显示 列出所有镜像id


docker image ls -q


--filter 配合 -q 产生出指定范围的 ID 列表,然后送给另一个 docker 命令作为参数,从而针对这组实体成批的进行某种操作的做法在 Docker 命令行使用过程中非常常见 列出只包含镜像ID和仓库名


docker image ls --format "{{.ID}}: {{.Repository}}"


以表格等距显示,并且有标题行


docker image ls --format "table {{.ID}}t{{.Repository}}t{{.Tag}}"


删除本地镜像

docker rmi 镜像/image id

docker image rm 镜像

删除容器

docker rm ubuntn:18.04

docker rm container_id

docker container rm 容器

列出完整镜像摘要

docker image ls --digests

删除所有仓库名为 ubuntu 的镜像

docker image rm $(docker image ls -q ubuntu)

删除所有在 mongo:3.2 之前的镜像:


docker image rm $(docker image ls -q -f before=mongo:3.2)


使用 Dockerfile 定制镜像

FROM 指定基础镜像

FROM 就是指定基础镜像,因此一个 Dockerfile中 FROM 是必备的指令,并且必须是第一条指令。

Docker存在一个特殊的镜像,名为 scratch。

这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。


FROM scratch
...


如果以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

RUN 执行命令

  • shell 格式:RUN <命令>
  • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。

在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。

镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

构建镜像 docker build 命令进行镜像构建。其格式为:


docker build [选项] <上下文路径/URL/->


镜像构建上下文(Context)

当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

命令 docker build -t nginx:v3 . 中的这个 .,实际上是在指定上下文的目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。

如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。

直接用 Git repo 进行构建


docker build https://github.com/chen2218/gitlab-ce-zh.git#:10.8


这行命令指定了构建所需的 Git repo,并且指定默认的 master 分支,构建目录为 /8.14/, 然后 Docker 就会自己去 git clone 这个项目、切换到指定分支、并进入到指定目录后开始构建。

tar 压缩包构建


docker build http://demo/context.tar.gz


从标准输入中读取 Dockerfile 进行构建

docker build - < Dockerfile

cat Dockerfile | docker build -

从标准输入中读取上下文压缩包进行构建


docker build - < context.tar.g


Dockerfile 指令

COPY

COPY ["<源路径1>",... "<目标路径>"]

<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR

目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。 此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能

如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。

如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解比如 <源路径> 可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去。

下载后的文件权限自动设置为 600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。

所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。

CMD


shell 格式:CMD <命令>
exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
参数列表格式:CMD ["参数1", "参数2"...]。


在指定了 ENTRYPOINT 指令后,用 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 数组,因此一定要使用双引号 ",而不要使用单引号。 如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如: CMD echo $HOME 在实际执行中,会将其变更为: CMD [ "sh", "-c", "echo $HOME" ] 这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。

一些初学者将 CMD 写为: CMD service nginx start

正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如: CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。

当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:


<ENTRYPOINT> "<CMD>"


可以写一个脚本,然后放入 ENTRYPOINT 中去执行,而这个脚本会将接到的参数(也就是 <CMD>)作为命令,在脚本最后执行。比如官方镜像 redis 中就是这么做的:


FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD [ "redis-server" ]


可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINT 为 docker-entrypoint.sh脚本。


#!/bin/sh
...
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
	chown -R redis .
	exec su-exec redis "$0" "$@"
fi

exec "$@"


该脚本的内容就是根据 CMD 的内容来判断,如果是 redis-server 的话,则切换到 redis 用户身份启动服务器,否则依旧使用 root 身份执行。 比如:


$ docker run -it redis id
uid=0(root) gid=0(root) groups=0(root)


ENV 格式有两种:


ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...


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

比如在官方 node 镜像 Dockerfile 中,就有类似这样的代码:

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


在这里先定义了环境变量 NODE_VERSION,其后的 RUN 这层里,多次使用 $NODE_VERSION 来进行操作定制。

可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0 即可,Dockerfile 构建维护变得更轻松了。 下列指令可以支持环境变量展开: ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD。

VOLUME


VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>


例如:


docker run -d -v mydata:/data mysql:5.7.26


EXPOSE


格式为 EXPOSE <端口1> [<端口2>...]


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

此外,在早期 Docker 版本中还有一个特殊的用处。以前所有容器都运行于默认桥接网络中,因此所有容器互相之间都可以直接访问,这样存在一定的安全性问题。于是有了一个 Docker 引擎参数 --icc=false,当指定该参数后,容器间将默认无法互访,除非互相间使用了 --links 参数的容器才可以互通,并且只有镜像中 EXPOSE 所声明的端口才可以被访问。这个 --icc=false 的用法,在引入了 docker network 后已经基本不用了,通过自定义网络可以很轻松的实现容器间的互联与隔离。

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口>

WORKDIR

格式为 WORKDIR <工作目录路径>。

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。

之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。 因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。

在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。