可以通过两种方式来构建 Docker 镜像:
- 修改容器中的现有镜像来构建镜像;
- 定义并执行 Dockerfile 脚本来构建镜像。
从容器构建镜像
从容器构建镜像的典型方式包含三个步骤:
- 从现有镜像中创建容器;
- 修改容器的文件系统;
- 通过
docker commit <容器> <新镜像名>
提交更改并创建新镜像。
# 创建一个容器,并写入 `HelloWorld` 文件(即修改了文件系统)
docker run --name hw_container ubuntu:latest touch /HelloWorld
# 将修改提交,并构建一个新镜像 `hw_image`
docker commit hw_container hw_image
# 通过新镜像创建容器,并检查 `HelloWorld` 文件
docker run --rm hw_image ls -l /HelloWorld
可以通过 docker container diff
查看容器中文件系统中的所有变更,包括文件及目录的添加、修改、删除:
docker run --name tweak busybox:latest rm /bin/vi
docker container diff tweak
# 输出为:
# C /bin
# D /bin/vi
执行结果中,A
表示新增文件,C
代表文件修改,D
代表文件删除。
执行 docker commit
时,除了提交文件系统的修改,还会提交执行上下文的元数据。以下创建容器的参数会作为元数据一并提交:
- 环境变量
- 工作目录
- 暴露的端口集合
- 卷
- 入口点
- 命令和参数
下面示例将构建一个镜像,并将 Git 程序作为容器的入口点:
# 创建一个容器(注意:`/bin/bash` 会被作为元数据)
docker run -it --name image-dev ubuntu:latest /bin/bash
# 在容器中安装 Git 程序
apt-get update
apt-get -y install git
exit
# 构建一个包含 Git 程序的新镜像
docker container commit -a "@dockerincation" -m "Added git" \
image-dev ubuntu-git
# 创建一个容器,并将 Git 作为入口点(入口点将作为元数据)
docker container run --name cmd-git --entrypoint git ubuntu-git
# 构建一个以 Git 程序作为入口点的镜像
docker commit -m "Set CMD git" -a "@dockerincation" \
cmd-git ubuntu-git # 这里使用了与上面相同的镜像名
docker rm -vf cmd-git # 清理容器
# 创建一个容器,并执行 `git version`
docker run --rm ubuntu-git version
深入研究镜像和层级
容器的根文件系统是由镜像提供的,由联合文件系统 UFS 实现的。
联合文件系统由层级组成,它通过写时复制(copy-on-write)机制来修改文件。当修改文件时,会先将原文件复制到一个新层级,然后对新层级中的副本进行修改。
当从联合文件系统读取文件时,容器会从最顶层读取文件,如果没有找到,则会向下一层一层地去找。而文件的添加、修改、删除操作会在最顶层中通过写时复制机制执行:
(F) (F) (F) (F) (F) (F) 最顶层/可写层级
| | ↓ ↓ ↓ ↓
[D] [D] [D] | | [A] [C] [C] [C] 层级2
| | | | ↓ | | |
[D] | [A] [C] | [A] | [A] [D] 层级1
| | | ↓ | |
[A] [A] [A] [A] [A] [A] 层级0
图中
A
表示新增文件,C
代表文件修改,D
代表文件删除。
当通过 docker commit
创建镜像时,实质是 将最顶层的更改项和元数据提交给镜像 。其中,层级的元数据包括层级的标识符、下一层级的标识符、容器的执行上下文信息。提交层级时,系统会生成新的层级 ID(也是镜像 ID)。
可以用 docker image history
查看镜像的层级历史记录。
仓库与标签
在 Docker 注册表中,每个仓库实际存放着一组层级结构的镜像。同时,每个仓库包含至少一个标签,并指向特定的层级标识符。
拉取仓库时若没有指定标签,Docker 则会尝试拉取标记为 latest 的镜像。若在 docker pull
使用了 --all-tags
选项,则会拉取仓库中所有带标签的镜像。
仓库和标签可通过 docker tag
、docker commit
、docker build
创建,如:
# 通过容器 `mod_ubuntu` 构建 `myuser/myfirsstrepo` 仓库,并打上 `mytag` 标签
docker commit mod_ubuntu myuser/myfirstrepo:mytag
# 从 `myuser/myfirstrepo:mytag` 复制一份镜像至 `myuser/mod_ubuntu`
docker tag myuser/myfirstrepo:mytag myuser/mod_ubuntu
默认情况下,每个仓库都包含 latest 标签,当命令中缺省标签时,将默认使用 latest 标签。
导入导出平面文件系统
由于联合文件系统的机制,每一次的修改都会让新镜像体积变大,即便是删除文件的操作。
可以用 docker image save
将镜像保存到 TAR 文件中,然后用 docker image import
将镜像文件导回 Docker ,从而让镜像扁平化。也可以通过 docker container export
从容器直接导出镜像文件。
docker import
可以识别多种压缩或未压缩的 tarball 文件。可以自定义一个 tarball 文件,然后通过 docker import
传输到一个新的镜像:
# 假设有一个 `hello` 可执行文件
./hello # 输出 `hello`
# 将 `hello` 转为 tarball 文件
tar -cf static_hello.tar hello
# 导入文件并构建新镜像,其中 `-` 代表标准输入流传入压缩包
docker import -c "ENTRYPOINT [\"/hello\"]" - print_hello < static_hello.tar
# 创建容器
docker run --rm print_hello # 输出 `hello`
版本控制最佳实践
Docker 维护统一软件的多个版本的关键点在于正确地设置仓库的标签。
务实的标签框架的核心为:每个仓库包含多个标签,并且多个可以引用同一镜像 。比如,可以为 golang 的最新版本 1.10.2 的镜像添加这些标签:1
、1.10
、1.10.2
、latest
。