3、Docker镜像基础:镜像分层原理、查看与管理本地镜像、从Docker Hub拉取镜像
聊到Docker,镜像绝对是绕不开的核心概念。我刚开始接触容器化时,总觉得镜像就是个“黑盒子”——把应用和依赖打包进去就完事了。直到有一次线上环境出了个诡异的依赖冲突,我才真正去研究镜像内部的结构。嗯,今天咱们就把这个“黑盒子”拆开看看。
3.1 镜像分层原理:为什么Docker镜像这么“省”
说白了,Docker镜像不是一整块大文件,而是由一层层只读文件系统堆叠起来的。每一层都代表一个Dockerfile中的指令。比如你写了个RUN apt-get update,就会生成新的一层。
为什么会这样设计?我打个比方:你想想看,假如你有一本厚厚的书,每次修改都要重印整本书,那多浪费?Docker的做法是——每次只印一张“透明胶片”,叠在原来的书上。这样多个镜像可以共享底层的胶片,只有顶层的差异部分才占用新空间。
我在项目中遇到过这样一个场景:公司有十几个微服务,基础镜像都是Ubuntu 20.04 + Python 3.9。如果每个服务都独立打包,光基础层就要占十几GB。但用了分层机制后,所有服务共享同一份基础层,实际占用只有2GB出头。这就是分层复用的威力。
核心要点:镜像由多层只读层组成,每一层对应Dockerfile的一条指令。层与层之间通过UnionFS(联合文件系统)合并,对外呈现为一个完整的文件系统。
3.2 查看与管理本地镜像:你的“镜像仓库”怎么管
本地镜像的管理,说白了就是几个命令的事。但我建议你养成定期清理的习惯——不然硬盘分分钟被撑爆。
3.2.1 查看本地镜像列表
docker images
这个命令会列出所有本地镜像,包括仓库名、标签、镜像ID、创建时间和大小。我个人习惯加个-a参数,把中间层镜像也显示出来——虽然平时用不到,但排查问题时很有用。
3.2.2 查看镜像详细信息
docker inspect <镜像名或ID>
这个命令会输出一个超长的JSON,包含镜像的完整元数据。我记得有一次排查一个镜像启动失败的问题,就是通过inspect发现它的Entrypoint指向了一个不存在的脚本。嗯,这种坑踩过一次就记住了。
3.2.3 查看镜像分层历史
docker history <镜像名>
这个命令特别有意思——它会展示镜像的每一层是怎么构建出来的,包括每层的大小和创建指令。我曾经用这个命令帮同事分析一个“臃肿”的镜像,发现他在Dockerfile里连续写了三个RUN命令,每个都下载了同样的依赖包。合并成一个RUN后,镜像体积直接砍掉了一半。
3.2.4 删除本地镜像
docker rmi <镜像名或ID>
注意:如果镜像正在被容器使用,直接删除会报错。需要先停止并删除容器,或者加-f强制删除(不推荐)。
我曾经犯过一个错误:在CI/CD脚本里用docker rmi -f强制删除所有镜像,结果把正在运行的容器也搞崩了。后来我改用docker image prune来清理未被使用的镜像,安全多了。
3.2.5 给镜像打标签
docker tag <源镜像> <新标签>
标签管理是版本控制的基础。我建议你遵循项目名:版本号-环境的命名规范,比如myapp:v1.2.3-prod。这样一眼就能看出是哪个环境、哪个版本。
3.3 从Docker Hub拉取镜像:你的“镜像超市”
Docker Hub就像个镜像超市,里面啥都有。但怎么挑、怎么拉,还是有些门道的。
3.3.1 基本拉取命令
docker pull <镜像名>:<标签>
如果不指定标签,默认拉取latest。但我强烈建议你——永远不要在生产环境用latest。你想想看,今天拉的是v1.0,明天拉的可能就是v2.0了,依赖一变,应用可能就崩了。
3.3.2 拉取指定版本
docker pull nginx:1.25.3
docker pull python:3.11-slim
我个人习惯用slim或alpine版本,体积小、攻击面少。比如Python官方镜像,完整版1GB多,slim版只有200MB,alpine版更是只有50MB。对于生产环境,能省则省。
3.3.3 拉取私有仓库的镜像
docker login <私有仓库地址>
docker pull <私有仓库地址>/<项目名>:<标签>
我在公司搭建过Harbor私有仓库,用来存放内部构建的镜像。拉取前需要先登录,密码建议用token而不是明文密码——安全第一。
3.3.4 拉取镜像的“避坑指南”
小技巧:拉取大镜像时,可以加--platform参数指定架构,比如docker pull --platform linux/amd64 nginx。我在Mac M1上拉取x86镜像时经常用这个参数。
我曾经遇到过一个问题:在树莓派(ARM架构)上拉取了一个x86的镜像,结果容器启动就报exec format error。后来才意识到是架构不匹配。所以拉取前一定要确认镜像支持的架构。
3.4 镜像分层原理的实战验证
光说不练假把式。咱们用docker history来验证一下分层原理:
# 拉取一个简单的nginx镜像
docker pull nginx:alpine
# 查看它的分层历史
docker history nginx:alpine
你会看到类似这样的输出:
IMAGE CREATED CREATED BY SIZE
abc123def456 2 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
def456ghi789 2 weeks ago /bin/sh -c #(nop) STOPSIGNAL SIGQUIT 0B
ghi789jkl012 2 weeks ago /bin/sh -c #(nop) EXPOSE 80 0B
jkl012mno345 2 weeks ago /bin/sh -c set -x && addgroup -g 101 -S … 1.2MB
mno345pqr678 2 weeks ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.25.3 0B
pqr678stu901 2 weeks ago /bin/sh -c #(nop) LABEL maintainer=NGINX Do… 0B
stu901vwx234 2 weeks ago /bin/sh -c #(nop) CMD ["sh"] 0B
vwx234yza567 2 weeks ago /bin/sh -c #(nop) ADD file:abc123... in / 7.05MB
看到了吗?每一层都对应一个Dockerfile指令,而且只有真正修改了文件系统的层才有大小(比如安装包、复制文件)。像CMD、EXPOSE这些元数据指令,大小是0B——它们只修改镜像的配置,不修改文件系统。
总结一下:镜像分层是Docker的精髓。它让存储更高效、传输更快、构建更灵活。理解了这个原理,你就能更好地控制镜像体积、优化构建速度,也能更从容地排查镜像相关的问题。
好了,镜像基础就聊到这儿。下一章咱们会深入Dockerfile的编写技巧——怎么写出既小又安全的镜像。到时候见!