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