1. 镜像到底是什么:先弄懂概念再动手
很多人接触 Docker 时,第一个接触的词就是 image,也就是镜像。但说实话,不少人在用了一两个月 Docker 之后,对镜像的理解仍然停留在"好像是个模板"这种模糊状态。一旦遇到镜像删不掉、构建不成功、或者容器跑不起来的时候,就完全不知道从哪里排查。
镜像其实可以理解成"一个打包好的操作系统文件系统快照",里面包含了程序运行所需的代码、运行时、系统工具、依赖库、配置文件等等。你运行一个容器,本质上就是把这个镜像当成一个独立的根目录,然后通过 Linux 内核的命名空间和 cgroup 机制,让里面跑起来的进程以为自己独占了一台机器。
但镜像和虚拟机镜像有一个本质区别:Docker 镜像是分层的。
Dockerfile 里每一行指令,比如 RUN apt-get install、COPY、ENV,都会生成一个新的镜像层。每一层只记录变化的部分,而不是完整记录整个文件系统。这意味着如果你拉取了两个共享基础镜像(比如都基于 ubuntu:22.04)的镜像,它们会共用底层的那些层,节省磁盘空间,也加速拉取传输。
我在实际工作中经常用一个类比跟同事解释:镜像就像积木搭出来的成品,每一层积木都是只读的。如果你想调整某个文件,不能直接在成品上改,只能重新搭一层新积木盖上去,把之前的内容"遮住"。容器运行的时候,Docker 会在镜像的最上面加一层可写的容器层,你在容器里做的所有修改都发生在这一层,一旦容器删除,这层可写层也跟着消失。
明白了这个概念,后面所有镜像相关的命令就好理解了。这篇文章我按日常使用频率,把镜像相关的命令从头到尾捋一遍,包含了参数解释、操作示例,还有我踩过的一些坑,希望能帮你少走弯路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 最常用的镜像命令清单与参数详解
2.1 docker images:查看本地镜像
这是你几乎每天都会敲的命令。不加任何参数直接执行:
bash复制docker images
输出大致长这样:
code复制REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 605c77e624dd 2 weeks ago 142MB
redis 7.2 5571a2a2d3a4 3 weeks ago 117MB
ubuntu 22.04 2dc39ba0599c 4 weeks ago 77.8MB
四列信息分别是仓库名、标签、镜像 ID 和大小。仓库名加标签合起来就是镜像的完整名称,比如 nginx:latest,其中 latest 是默认标签,代表最近一次构建的版本,但不一定是最新的稳定版,这是一个很常见的误解,后面会专门讲。
日常用得多的参数有这几个:
-a,显示所有镜像,包括中间层镜像。中间层镜像是构建过程中产生的临时层,正常情况会被隐藏,如果你需要调试或者清理它们,用-a查看。-q,只显示镜像 ID。这个参数在写自动化脚本时非常有用,比如你想删掉所有虚悬镜像,可以配合其他命令一起用。--format,自定义输出格式。比如我只想看到仓库名和标签:docker images --format "{{.Repository}}:{{.Tag}}",这在脚本里处理文本和批量操作时比解析默认表格靠谱得多。--filter,按条件过滤。docker images --filter dangling=true可以找出所有虚悬镜像,这些是构建新镜像时遗留下来的旧版本层,占用空间,但已经没有任何标签引用了。
dangling 这个词值得展开说一下。当你反复构建同一个镜像时,旧的镜像层如果不再被任何标签引用,就会变成虚悬状态,镜像名和标签显示为 <none>:<none>。这些镜像不会自动删除,积攒多了非常占磁盘。我见过一个同事的开发机,光虚悬镜像就占了三四十 GB,磁盘告警才发现。
2.2 docker pull:拉取镜像
本地没有的镜像,用 docker pull 从镜像仓库拉下来。最简单的用法:
bash复制docker pull nginx
等价于:
bash复制docker pull docker.io/library/nginx:latest
完整写法包含了三个部分:[仓库地址]/[命名空间]/[镜像名]:[标签]。默认仓库地址是 docker.io 也就是 Docker Hub,默认命名空间是 library,默认标签是 latest。
拉取指定版本的时候要注意,不是所有镜像都规范维护了 latest 标签,尤其是企业级中间件,很多项目方明确建议你拉取带版本号的标签。例如 Redis 官方镜像,你可以拉取 redis:7.2-alpine 这种带 Alpine Linux 精简版的标签,体积比完整版小很多,非常适合开发和轻量环境。
docker pull 值得记住的参数是 -q,静默拉取不输出进度条,在脚本中很有用。另外 --platform 参数可以用来指定拉取哪个平台的镜像,比如你在 x86 机器上想拉取 arm64 架构的镜像做交叉验证:
bash复制docker pull --platform linux/arm64 ubuntu:22.04
一条命令就把目标平台镜像拉下来了,不需要专门找一台 ARM 机器。这个参数在排查多架构镜像问题时特别好用。
2.3 docker rmi:删除镜像
rmi 是 remove image 的缩写。删除镜像的命令是:
bash复制docker rmi nginx:latest
或者直接指定镜像 ID:
bash复制docker rmi 605c77e624dd
如果该镜像正在被某个容器使用,你会收到一个报错:
code复制Error response from daemon: conflict: unable to remove repository reference "nginx:latest" (must force) - container abc123 is using its referenced image abc123
这个报错很常见,意思是容器还在占用这个镜像。解决办法是先停掉并删除容器,再删除镜像,或者使用 -f 强制删除。但我强烈不建议碰见报错就顺手加个 -f,因为强制删除可能会导致运行中的容器出现问题,正确做法是先定位是哪个容器在用,处理完容器再删镜像。
批量删除的场景也很常见。比如你想删除所有虚悬镜像:
bash复制docker image prune
这个命令会提示你确认,加 -f 可以跳过确认直接删。docker image prune -a 会删除所有没有被容器使用的镜像,注意是"所有",慎用。
如果想让本地镜像"瘦身",我一般先执行 docker image prune -f 清理虚悬层,再执行 docker ps -a 检查有没有不再需要的容器,把容器清理干净之后,再按需逐批删除镜像。盲目一次性 docker system prune -a -f 会把你本地所有镜像、容器、网络、缓存全部清掉,如果你正好缓存了几个需要离线使用的大镜像,哭都来不及。
2.4 docker tag:镜像打标签
标签在 Docker 里天然就是为版本管理设计的。docker tag 的作用是给本地已存在的镜像创建一个新的标签引用,就像给同一个文件创建了一个快捷方式,并不会复制镜像数据。命令格式:
bash复制docker tag 源镜像[:标签] 目标镜像[:标签]
很典型的场景:你要把本地构建的镜像推送到自己的私有仓库,比如 Harbor 或阿里云 ACR:
bash复制docker tag myapp:1.0.0 registry.example.com/dev/myapp:1.0.0
docker push registry.example.com/dev/myapp:1.0.0
另一个很实际的用法:给部署到生产环境的镜像打一个长期保留标签。比如你某一天从 myapp:latest 启动了一个容器,确认这个版本运行稳定,想把它固定下来,可以这样操作:
bash复制docker tag myapp:latest myapp:release-2024-06-01
这样后续 latest 标签即使被更新,你仍然可以通过 myapp:release-2024-06-01 精确找回这个稳定版本。
这里插一句,我在实际项目里对 latest 标签的态度是"能不用就不用"。因为 latest 指向什么版本,完全取决于上游维护者什么时候执行 push 操作,拉取结果不可复现。生产环境的镜像版本必须显式指定,比如 myapp:1.2.3,否则哪次部署拉到了刚更新的 latest,出了问题连回滚都不知道该回退到哪个版本。
3. 进阶命令:构建、提交、导入导出
3.1 docker build:从 Dockerfile 构建镜像
绝大部分自定义镜像都要通过 docker build 构建。这也是我工作中敲得最多的命令之一。
最简单的构建方式:
bash复制docker build -t myapp:1.0.0 .
命令末尾的 . 指的是构建上下文路径,即当前目录。构建上下文会整个打包发送给 Docker 守护进程,所以这个目录不要太大,并且养成好习惯,在项目根目录添加 .dockerignore 文件,把 node_modules、target、.git 这些目录排除掉,否则构建速度会明显变慢,网络传输浪费严重。
常用参数:
-t或者--tag,给镜像指定仓库名和标签,可以多次使用-t同时打多个标签。-f,指定 Dockerfile 路径。默认会查找构建上下文根目录下的 Dockerfile,如果 Dockerfile 不在当前目录,就需要用-f指定。--build-arg,传入 Dockerfile 中定义的变量,例如--build-arg VERSION=1.2.3。这在多环境构建时特别方便,不需要修改 Dockerfile 就能打不同环境的镜像。--no-cache,禁用构建缓存。当你怀疑缓存导致构建结果不对时,用这个参数强制重新构建所有层。--platform,交叉构建指定平台的镜像。比如在 x86 上构建 ARM 镜像:docker build --platform linux/arm64 -t myapp:arm64 .--load和--push,这两个参数通常配合--platform和 BuildKit 使用,分别表示将构建产物加载到本地镜像列表里或者直接推送到远程仓库。
需要特别说一下构建缓存机制。Docker BuildKit 在执行 Dockerfile 时,如果某个指令的前置层没有发生变化,就会复用已有的构建缓存,这样可以大幅缩短重复构建的时间。但缓存复用偶尔也会带来问题,最常见的是执行 RUN apt-get update && apt-get install 时,因为源更新导致缓存命中后装不到最新软件包。我自己的处理习惯是:关键系统依赖安装前用 --no-cache 强制构建,或者把包版本固定写在 Dockerfile 里,避免不确定性。
3.2 docker commit:从容器生成镜像
docker commit 可以把一个容器的当前状态打包成一个新的镜像。命令格式:
bash复制docker commit [选项] 容器ID 新镜像名[:标签]
举个例子,你启动了一个容器,手动在里面安装了一些软件、修改了配置文件,想把当前环境固化下来:
bash复制docker commit -m "add vim and configure nginx" -a "yourname" my_container myapp:with-tools
-m 是提交说明,-a 是作者信息。
但我必须先强调:docker commit 不是一种推荐的做法。因为它会丢失容器层的历史和可追溯性,别人拿到这个镜像之后,根本无法知道你在这个容器里做了什么操作,这跟 Dockerfile 的可声明、可审查、可复现理念相悖。我在实际工作中只会在两种场景下用 docker commit:
一是调试阶段临时把容器现状保存下来,防止调试到一半被误删。二是排查一个容器内手动操作后的状态,需要给同事复现时,快速生成一个"现状快照"。
正常情况下,都应该通过编写 Dockerfile 来构建镜像。docker commit 生成的镜像体积往往很臃肿,里面塞满了临时文件和无意义的历史状态。你真正想固化容器状态的时候,先想想到底是哪个文件、哪项配置是必要的,把它们写进 Dockerfile 里,然后重新构建,这才是长久之计。
3.3 docker save 与 docker load:离线迁移镜像
docker save 和 docker load 是成对出现的,作用是把镜像打包成 tar 文件,以及从 tar 文件还原镜像。典型应用场景是离线环境迁移,比如内网服务器不能直接访问 Docker Hub,你需要在一台能访问外部网络的机器上拉取镜像,打包拷过去再导入。
导出单个镜像:
bash复制docker save -o nginx.tar nginx:latest
导出多个镜像到同一个 tar 包:
bash复制docker save -o images.tar nginx:latest redis:7.2
在目标机器上还原:
bash复制docker load -i nginx.tar
实测下来,我在生产环境做离线部署时比较喜欢的方式是先把需要的镜像统一构建并打标签,然后用 docker save 打包成一个总包,内网目标机器通过 docker load 一次导入,再配合 compose 脚本一键拉起所有服务。这样省去了一台一台 push/pull 的麻烦。
需要注意一点:docker save 导出的是镜像本体(包含所有层和元数据),不是容器快照。如果你想迁移一个带运行状态的容器,需要先把容器 commit 成镜像,再 save,否则容器里的运行状态不会被保留。
3.4 docker push 与私有仓库
docker push 是把本地镜像推送到镜像仓库的命令。推送前必须确保本地镜像已经用仓库地址打好了标签,否则 Docker 不知道往哪推。
一个完整的推拉闭环是:
bash复制docker pull ubuntu:22.04
docker tag ubuntu:22.04 registry.example.com/dev/ubuntu-custom:22.04
docker push registry.example.com/dev/ubuntu-custom:22.04
很多人在第一次推送时容易碰到认证问题,提示 no basic auth credentials,这是因为没有先执行登录操作。Docker Hub 或私有仓库都需先执行:
bash复制docker login registry.example.com
输入用户名密码后,凭证会保存在 ~/.docker/config.json 里,后续推送拉取就不需要重复登录了。在 CI/CD 环境里,一般通过 docker login -u 用户名 -p 密码 的方式在流水线里注入凭证,但要注意明文密码泄露的问题,推荐使用各 CI 平台的 secret 功能存放密码。
对于自建私有仓库,轻量方案是运行一个 registry:2 容器:
bash复制docker run -d -p 5000:5000 --name registry registry:2
之后本地镜像想推到这个仓库:
bash复制docker tag myapp:1.0.0 localhost:5000/myapp:1.0.0
docker push localhost:5000/myapp:1.0.0
这里有一个容易踩的坑:如果没有给 Docker daemon 配置 insecure-registries,默认只信任 HTTPS 仓库,用 HTTP 协议访问私有仓库会被拒绝。开发环境可以通过修改 Docker Desktop 的配置把 http://localhost:5000 加进 insecure registries,生产环境我还是建议直接上 HTTPS 证书,别省这一步。
4. 镜像排查与常用调试技巧
4.1 docker history:看镜像的分层内幕
docker history 是我排查镜像问题时最喜欢的命令之一。它能看到一个镜像是怎么一步步构建出来的,每一层分别执行了什么操作,占用了多少空间:
bash复制docker history nginx:latest
输出大概长这样:
code复制IMAGE CREATED CREATED BY SIZE
605c77e624dd 2 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon off;"] 0B
...
每一行对应 Dockerfile 里的一条指令,SIZE 是这一层新增的磁盘占用。如果你发现某个镜像体积异常大,用 docker history 一眼就能定位到是安装依赖那层撑起来的,还是复制了某个超大目录进去导致的。
在实际排障中,有一次我发现某个 Java 镜像体积高达 1.2GB,用 docker history 逐层看,发现是一行 COPY target/app.jar /app/app.jar 之后紧接着一个清理命令写错了路径,导致下载的安装包根本没有被删除,白白多了 400MB。这种问题用别的命令很难定位,只看 Dockerfile 也不容易发现,docker history 直接看每层的尺寸,问题一目了然。
还能配合 --no-trunc 参数查看完整命令,不会被截断:
bash复制docker history --no-trunc myapp:1.0.0
4.2 docker inspect:查看镜像的完整元数据
docker inspect 可以查看镜像或容器的底层元数据,输出是一大段 JSON。日常调试可能用不到全部,但有几个字段确实很重要。
查看镜像的架构和系统信息:
bash复制docker inspect myapp:1.0.0 | grep -A 10 "Architecture"
在排查"为什么这个镜像在我的机器上跑不起来"的时候,先看一眼架构是不是对上了。比如 arm64 的镜像在 x86 环境上直接运行一般会报 exec format error,很多人第一反应是程序问题,其实检查一下架构就明白了。
docker inspect 还经常用来查看容器的挂载信息、网络模式、环境变量等:
bash复制docker inspect 容器ID | grep -A 20 "Mounts"
如果只想精确获取某个字段,推荐不要用 grep 硬匹配,用 --format 提取更稳:
bash复制docker inspect --format='{{.Config.Env}}' 容器ID
docker inspect --format='{{.Architecture}}' myapp:1.0.0
这种方式在自动化脚本里使用非常可靠,不会因为 JSON 格式变化导致解析失败。
前一阵子我排查一个"容器启动后立即退出"的问题,就是通过 docker inspect 看容器日志路径指向的 JSON 文件,发现被日志轮转清掉了,配合查看 State.ExitCode 和 State.Error,最终定位到启动脚本里一个编码问题。可以说 docker inspect 是 Docker 排障的底层入口,无论容器出任何问题,先 inspect 一下,信息量比想象中大得多。
4.3 常见错误:unable to find image 的完整排查思路
很多新手刚学 Docker 时第一个碰到的报错就是:
code复制Unable to find image 'hello-world:latest' locally
docker: Error response from daemon: pull access denied for hello-world, repository does not exist or may require 'docker login'
这句话前半句其实是正常的,Docker 在本地找不到镜像时,会尝试去仓库拉取,如果你敲的名字拼错了、或者仓库里根本没有这个镜像,就会出现后面的报错。出现这个问题的常见原因有几个:
第一,镜像名拼写错误。比如把 nginx 拼成 nignx,或者版本号写错,仓库里没有对应的标签。这种问题最简单,重新确认名字即可。
第二,网络不通畅,导致从 Docker Hub 拉取超时。国内环境拉到一半卡住的情况非常多,这种时候可以配置镜像加速器,或者走代理(此处不展开,按本地实际操作即可)。
第三,权限问题。有些镜像位于私有仓库,不登录拉取会直接拒绝,报 pull access denied。解决办法就是先 docker login,确认账号有权限。
第四,仓库地址缺省导致拉错了地方。比如你以为自己在拉公司私有仓库的镜像,结果因为本地没有提前 login 或是没有在镜像名里写全仓库地址,Docker 默认去 Docker Hub 找,自然找不到。
排查这种问题我习惯分三步走:先 docker search 镜像名 看仓库里到底存不存在这个镜像名,再确认标签是否存在,最后确认本机 DNS 和网络是否能正常访问仓库地址。很多时候问题都不在 Docker 命令本身,而是在最基础的环境层。
4.4 docker image 子命令速查表
从 Docker 1.13 开始,Docker 官方逐步推动命令从 docker xxx 形式转向 docker xxx subcommand 形式。对于镜像操作,就是 docker image 开头的一系列子命令。两者的关系基本上是一一对应的:
| 传统命令 | docker image 子命令 | 作用 |
|---|---|---|
| docker pull | docker image pull | 拉取镜像 |
| docker push | docker image push | 推送镜像 |
| docker images | docker image ls | 列出本地镜像 |
| docker rmi | docker image rm | 删除镜像 |
| docker build | docker image build | 构建镜像 |
| docker tag | docker image tag | 打标签 |
| docker history | docker image history | 查看镜像历史分层 |
| docker inspect | docker image inspect | 查看镜像元数据 |
| docker save | docker image save | 导出镜像 |
| docker load | docker image load | 导入镜像 |
| docker commit | docker container commit | 容器提交为镜像 |
新命令有更好的可读性和分类逻辑,但我自己也承认,因为手速和习惯,日常还是经常敲 docker images 而不是 docker image ls。这不影响使用,Docker 官方也承诺长期兼容原有命令。你需要记住的是,如果你在脚本或文档里看到新写法,要知道它和旧写法是一回事。
在写自动化脚本时,我建议统一用新版子命令,比如 docker image ls -q、docker image rm -f,这样语义更清楚,也方便未来迁移到其他容器工具时做封装。
5. 镜像管理实战经验与避坑清单
5.1 写好 Dockerfile,从源头控制镜像质量
镜像的命令操作再多,也只是"管理"层面的手段。真正决定镜像质量好坏的还是 Dockerfile。我在评审同事的 Dockerfile 时,最常见的毁镜镜像操作有三个:
第一,把所有依赖包一股脑装进生产镜像。正确做法是使用多阶段构建,在构建阶段安装编译工具,最终阶段只拷贝运行时二进制文件或 jar 包。比如用 Go 构建的场景:
dockerfile复制FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .
FROM alpine:3.19
COPY --from=builder /app/server /usr/local/bin/server
EXPOSE 8080
CMD ["server"]
最终镜像只有几十 MB,而如果把整个 Go 工具链都留在镜像里,体积轻松超过 1GB。
第二,在 RUN 指令里没有做好缓存清理。比如:
dockerfile复制RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
这里必须把 apt-get update 和安装过程合并到同一层,并且删除缓存文件,否则缓存层会导致镜像体积膨胀。我见过非常夸张的例子,一条 apt-get install 没清理缓存,镜像多了 300MB 的索引文件。
第三,把敏感信息写死在构建层里。比如:
dockerfile复制ENV DATABASE_PASSWORD=secret123
这样的镜像是将密码刻在每一层里的,通过 docker history 就可以翻出来,非常危险。正确做法是使用运行时环境变量注入,或者利用 Docker 的 secret 管理机制。
5.2 镜像清理策略
镜像积攒到一定量之后,磁盘空间肯定是问题。我建议把清理工作做成定时任务,而不是等到磁盘满了再手动去删。我自己习惯的清理策略分三个层级:
日常清理,每周执行一次:
bash复制docker image prune -f
这个命令只清理虚悬镜像,不影响正在使用的镜像,安全系数很高。
深度清理,每月执行一次:
bash复制docker container prune -f
先删掉所有已经停止的容器,再执行:
bash复制docker image prune -a -f
删除所有未被任何容器引用的镜像。这个操作就会比较激进,执行之前我会先确认是否需要保留某些离线镜像。
极端清理,磁盘告急时:
bash复制docker system prune -a -f --volumes
这个命令会把本地所有未使用的镜像、容器、网络、缓存和卷全部删掉。注意 --volumes 这个参数还会把匿名卷删掉,如果你的某些数据没有挂载出来,就再也找不回来了,除非你确认这些卷里的数据不重要,否则不要轻易带这个参数执行。我在生产环境一般只做前两个层级的清理,极端清理只在开发机上用过。
5.3 镜像命名与版本管理规范
镜像管理还有一个很容易被忽视的问题:命名和版本规范。团队协作时,如果每个人打标签的方式都不一样,镜像仓库很快就会变成一团乱麻。
我建议团队里明确几条规则:
镜像名要包含足够的信息。对于业务应用,按照 项目名/服务名:版本号 的格式命名;对于部署到生产环境的镜像,建议在标签里加上 git commit 短哈希,例如 order-service:1.2.0-a1b2c3d,这样可以精确定位到源码的某个提交。
版本号建议遵循语义化版本规范。主版本号不兼容更新时递增,次版本号增加新功能时递增,修订号修复 bug 时递增。发布前应该先打好标签推送确认无误,再把固定版本镜像部署到目标环境,避免直接使用 latest。
对于多环境部署,一个常见的做法是同一个镜像通过不同标签区分环境,比如:
bash复制docker tag myapp:1.2.0 myapp:test-1.2.0
docker tag myapp:1.2.0 myapp:prod-1.2.0
然后在不同环境的 compose 文件里引用对应标签,这样测试通过前不会影响生产,比直接全用 latest 可控得多。
5.4 给新手的三个实操建议
学镜像命令,不建议死记硬背。我自己带新人的时候,通常让他们先做三件事:
第一,在本地把官方镜像拉几个下来,比如 nginx、redis、ubuntu,逐个运行一遍 docker image inspect、docker image history,用肉眼看看镜像是怎么分层的。把输出和 Dockerfile 对应起来看,比背任何命令都直观。
第二,自己写一个简单的 Dockerfile,一个最小的 Python 应用或者前端静态页面,然后经历完整的构建、打标签、导出、导入、推送私有仓库流程。整套流程走一遍,你才能理解镜像在真实项目里是怎么分发和部署的。
第三,故意制造几个错误再排查掉。比如故意敲错镜像名、故意不登录就 push、故意构建一个超大镜像然后用 docker history 定位膨胀点。这种"挫折式学习"比顺向学习记忆深刻得多。
镜像是 Docker 的核心,把镜像相关的命令玩透了,你离真正熟练使用 Docker 就只差一层窗户纸。我在真实的项目里处理过不少镜像相关的故障,大部分都离不开这一类命令,掌握好它们,能让你在排查问题时比盲目搜索快上好几倍。
