2.《Docker 从入门到实践》镜像与Dockerfile

1 镜像的增删改查

1.1 镜像的获取与查看

docker pull ubuntu:18.04 # 从Docker Hub上下拉,获取镜像
docker run -it --rm ubuntu:18.04 bash # 运行镜像

docker image ls # 列出已经下载下来的镜像
docker system df # 查看镜像、容器、数据卷所占用的空间

docker image ls -f dangling=true # 列出所有的虚悬镜像 
docker image prune # 删除所有的虚悬镜像 

docker image ls -a # 列出所有的中间层镜像
docker image ls -f since=mongo:3.2 # 列出mongo:3.2之后建立的镜像
docker image ls --format "{{.ID}}: {{.Repository}}" # 列出镜像,只显示镜像ID和仓库名
docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}" # 表格等距显示

虚悬镜像(dangling image)

一类既没有仓库名,也没有标签的特殊镜像;一般发生在docker builddocker pull的执行过程,即重复命名的镜像会覆盖之前的镜像,而旧版镜像就会变为虚悬镜像

一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的

中间层镜像

一般也没有标签,但会是其他镜像的依赖;只要删除那些依赖它们的镜像后,这些依赖的中间层镜像也会被连带删除;中间层镜像的主要用途是为了加速镜像构建、重复利用资源

1.2 镜像的删除

docker image rm 501 # 删除特定镜像(id以501为开头的镜像)
docker image rm centos # 使用镜像名来删除镜像

docker image rm $(docker image ls -q redis) # 删除所有仓库名为redis的镜像
docker image rm $(docker image ls -q -f before=mongo:3.2) # 删除mongo:3.2之前的镜像

Untagged 和 Deleted

Untagged 是标签清理操作,当一个镜像的所有标签都被清理掉时,就会触发删除镜像的行为;但是如果该无标签镜像依然是其他镜像/容器的依赖时,该镜像也不会被删除(中间层镜像)

1.3 理解镜像的构成

docker run --name webserver -d -p 80:80 nginx # 启动一个nginx服务的容器
docker exec -it webserver bash # 进入容器并修改:

echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
exit # 退出容器

docker diff webserver # 对比前后容器的变动情况
docker commit \
    --author "Tao Wang <[email protected]>" \
    --message "修改了默认网页" \
    webserver \
    nginx:v2 # 保存修改后的容器为镜像

docker history nginx:v2 # 查看新镜像的历史(多了一层)

慎用 docker commit

docker commit 生成的镜像也被称为黑箱镜像,即内部的制作过程是未知的

docker commit 命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用 docker commit 定制镜像,定制镜像应该使用 Dockerfile 来完成(下一小节内容)

2 Dockerfile 入门

2.1 Dockerfile 定制镜像

镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本(Dockerfile),用这个脚本来构建、定制镜像

以上一节定制 nginx 镜像为例,对应的 Dockerfile 如下:

# Dockerfile
FROM nginx # 指定基础镜像
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

空白镜像 scratch

FROM scratch是一种特殊用法,意味着后续定制的镜像不以任何镜像为基础(让镜像体积更加小巧);该做法常用于不需要操作系统提供运行时支持的情况,比如 Linux 下静态编译的程序

Dockerfile 中每一个指令都会建立一层,但是 Docker 的联合文件系统(_Union File System_)会有最大层数限制(曾经是最大不得超过 42 层,现在是不得超过 127 层),所以指令的使用不能过于松散/频繁

为了确保每一层只添加真正需要添加的东西,一般在 Dockerfile 的最后最好有额外的清洗工作:删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件;示例如下:

FROM debian:stretch

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

使用 docker build 命令根据 Dockerfile 进行镜像的构建:

# docker build [选项] <上下文路径/URL/->
docker build -t nginx:v3 # -t 指定了最终镜像的名称
docker build -t nginx:v3 . # . 指定了上下文路径

# 支持直接从 Git repo 中构建,也支持从压缩包中构建
docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world
docker build http://server/context.tar.gz # 自动将压缩包解压后,作为上下文并开始构建

其他细节:

  • Dockerfile一般置于一个空目录下,或者项目根目录下
  • 支持类似.gitignore的语法.dockerignore,来剔除不必要的上下文

进阶文档:

  • Dockerfile 官方文档:https://docs.docker.com/engine/reference/builder/
  • Dockerfile 最佳实践文档:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
  • Docker 官方镜像 Dockerfile:https://github.com/docker-library/docs

2.2 Dockerfile 常用指令

  1. RUN:执行命令,支持shell格式和exec格式
    • 支持 Shell 类的行尾添加 \ 的命令换行方式,以及行首 # 进行注释的格式
    • exec格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式
  2. COPY:复制命令,只支持相对路径,依赖上下文(context) 目录
    • 格式1(类似于命令行):COPY [--chown=<user>:<group>] <源路径>... <目标路径>
    • 格式2(类似于函数调用):COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
    • 支持通配符:COPY hom?.txt /mydir/;可以通过--chown参数来改变文件的用户权限
  3. ADD:高级复制,类似于COPY,但是有额外的功能
    • 支持URL或压缩文件作为<源路径>,会自动下载/解压(不想解压的情况就用COPY
    • 官方更推荐使用COPY,因为语义更明确,而且ADD可能会影响镜像的构建(失败/变慢)
  4. CMD:启动命令,格式类似于RUN;用于指定默认的容器主进程的启动命令
    • 推荐使用exec格式,注意使用双引号 ",而不要使用单引号
    • 对于容器而言,其启动程序就是容器应用进程(区别于虚拟机)
    • 容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义
  5. ENTRYPOINT:入口点,功能类似于CMD,都是在指定容器启动程序及参数
    • 指定了 ENTRYPOINT 后,CMD 的含义就发生了改变(不再是直接的运行其命令)
    • 而是将 CMD 的内容作为参数传给 ENTRYPOINT ,即实际执行为<ENTRYPOINT> "<CMD>"
    • 场景一:让镜像变成像命令一样使用 ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]
    • 场景二:应用运行(启动主进程)前的准备工作,比如数据库的配置或初始化
  6. ENV:设置环境变量,格式为ENV <key1>=<value1> <key2>=<value2>...
    • 含有空格的值可以用双引号括起来 ENV NAME="Happy Feet"
  7. ARG:构建参数,格式为ARG <参数名>[=<默认值>]
    • 参数只在镜像构建时使用,将来容器运行时是不会存在这些参数
    • ARG 指令有生效范围,如果在 FROM 指令之前指定,那么只能用于 FROM 指令中
  8. VOLUME:定义匿名卷,格式为VOLUME ["<路径1>", "<路径2>"...]
    • 容器运行时应保持容器存储层不发生写操作,所以动态数据应该保存于卷(volume)中
    • 作为替代,挂载也可以发生在容器运行时:docker run -d -v mydata:/data xxxx
  9. EXPOSE:暴露端口,格式为 EXPOSE <端口1> [<端口2>...]
    • 声明容器运行时提供服务的端口(只是声明,方便使用者理解镜像服务的守护端口)
    • 端口不会因此而开启,但运行docker run -P时会随机映射EXPOSE暴露的端口
  10. WORKDIR:指定工作目录,不存在时会自动创建
  11. USER:指定当前用户(需事先创建)或用户组
  12. HEALTHCHECK:检查容器的健康状况(Docker 1.12后的新功能)
  13. ONBUILD:特殊条件,在镜像构建时不会执行,以当前镜像为基础镜像去构建镜像时才会被执行
  14. LABEL:以键值对的形式为镜像添加一些元数据

2.3 Dockerfile 多阶段构建

Docker v17.05 开始支持多阶段构建 (multistage builds)

  • 避免单文件部署可能存在的问题:镜像层次多,镜像体积较大,部署时间长
  • 没多文件部署那么复杂:需要额外的编译脚本来整合两个Dockerfile

原书中使用了一个面向 PHP 开发者的实战用例

为了更直观地理解,此处改为一个 Python 相关镜像的多阶段构建过程展示:

示例(参考链接

  • 目的:压缩Flask应用对应的镜像大小
  • 步骤1:先修改基础镜像(Ubuntu->alpine)
  • 步骤2:然后构建开发环境,编译和安装所需模块
  • 步骤3:最后构建生产环境,只复制需要的文件,减少空间
  • 结果:镜像大小从700多M压缩为102M
###########################################
# 首先构建一个编译环境用以把pycryptodome编译成whl文件
FROM python:3.8.8-alpine3.13 as build    # 编译环境镜像指定别名 build
WORKDIR /app                             # 指定工作目录,等下从该目录复制编译好的whl文件
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories  #把alpine源替换成清华大学源
RUN set -e \
   && apk add --no-cache gcc musl-dev g++ make libffi-dev openssl-dev git    #安装gcc编译环境              
# build
RUN pip wheel --no-deps pycryptodome==3.10.1          # 开始编译pycryptodome
RUN ls -al /app                                       # 查看一下工作目录,是否生成了whl文件

##########################################
# 然后构建正式镜像
FROM python:3.8.8-alpine3.13
LABEL maintainer="CFSoft Studio <[email protected]>, QQ: 360026606, wechat: 360026606"
COPY --from=build /app /                               # 从编译环境镜像中把编译好的文件复制到当前镜像 

# 复制源代码
COPY src /src/                                         # 把我的应用源代码复制进镜像 
COPY config /config                                    # 把配置文件复制进镜像 

# 安装上面编译的 whl文件和应用依赖的其他模块
RUN pip install pycryptodome-3.10.1-cp35-abi3-linux_x86_64.whl \                            
   && pip install -r /src/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

VOLUME /config                                                      

EXPOSE 5000

ENTRYPOINT ["python", "-u",  "/src/app.py"]

其他案例参考:

3 其他镜像相关

3.1 Docker 镜像支持多种系统架构

Linux x86_64 架构的系统中只能使用 Linux x86_64 的镜像创建容器

Windows、macOS 除外,其使用了 binfmt_misc 提供了多种架构支持,在 Windows、macOS 系统上 (x86_64) 可以运行 arm 等其他架构的镜像

官方镜像有一个 <code>manifest</code> 列表 (<code>manifest list</code>),能根据系统架构自动拉取对应的镜像

常见相关命令:

  • docker manifest inspect golang:alpine:查看manifest 列表
  • docker manifest create:创建 manifest 列表
  • docker manifest annotate:设置 manifest 列表
  • docker manifest push:推送到 Docker Hub

3.2 其他制作镜像的方式

方式1:从 rootfs 压缩包导入

  • 格式:docker import [选项] <文件>|<URL>|- [<仓库名>[:<标签>]]
  • 压缩包可以是本地文件、远程 Web 文件,甚至是从标准输入中得到
  • 压缩包将会在镜像 / 目录展开,并直接作为镜像第一层提交
  • 示例:创建一个 OpenVZ 的 Ubuntu 16.04 模板的镜像
docker import \
    http://download.openvz.org/template/precreated/ubuntu-16.04-x86_64.tar.gz \
    openvz/ubuntu:16.04

方式2:镜像归档文件的导入和导出

  • 使用 docker save 命令可以将镜像保存为归档文件
  • 使用 docker load 命令可以将归档文件加载为镜像
  • 不推荐使用,现在镜像迁移应该直接使用 Docker Registry

往年同期文章