概述
镜像命令一共就七个,但每个都有能让人卡半天的细节:tag 不写会拉到最新版、rmi 删不掉会报 conflict、save 出来的文件在离线机房才是唯一救命稻草。本文按"获取 → 查看 → 删除 → 导出导入 → 推送"的实际使用顺序拆一遍,配上真实报错和排查动作。
纲要
- 镜像命名 :
[repository]:[tag],tag 缺省即latest - 七个核心命令 :
pull/images/rmi/save/load/build/push - 两条获取路径 :从 Registry
docker pull,从 Dockerfiledocker build - 两条分发路径 :推到仓库
docker push,打成文件docker save+ U 盘 +docker load - 镜像流转全景:Mermaid 图表达四种流向
- 离线迁移实操 :用资料目录里的
mysql.tar(377 MB)走一遍内网交付 - 自查方法 :
docker xx --help比背命令管用 - 实战坑 :镜像名写错、
rmi被容器占用、save与export混用、空压缩包
镜像名到底怎么写
Docker 的镜像名固定两段:
text
[repository]:[tag]
│ │
│ └── 版本,不写就是 latest
└── 镜像仓库名,通常是软件名
mysql:5.7 和 mysql:5.6 是两个完全不同的镜像,ID、分层、体积都不一样。冒号不能省成横杠,写成 mysql-5.7 会被当成另一个仓库名去找,结果就是找不到。
tag 省略时 Docker 补 latest。这个默认值在生产上是隐患:
| 写法 | 实际解析 | 风险 |
|---|---|---|
docker pull nginx |
nginx:latest |
今天拉的是 1.25,下个月可能是 1.27,环境不可复现 |
docker pull nginx:1.21.6 |
精确版本 | 任何时间拉到的都是同一个镜像 |
docker pull nginx:alpine |
alpine 变体 | 体积从 187 MB 降到 40 MB 左右,但 shell 是 sh 不是 bash |
CI 里写死具体版本号,latest 只留给本地随手试。
还有一层:repository 前面可以带仓库地址,完整形态是 [registry host]/[项目或命名空间]/[repository]:[tag]。推私有仓库时必须写全,否则 Docker 默认往 DockerHub 推。
命令速查
| 命令 | 作用 | 示例 |
|---|---|---|
docker pull |
从镜像仓库拉取镜像 | docker pull nginx:1.21.6 |
docker images |
列出本地已有镜像 | docker images |
docker rmi |
删除本地镜像(remove image) | docker rmi nginx:latest |
docker save |
把镜像导出成 tar 包文件 | docker save -o nginx.tar nginx:latest |
docker load |
从 tar 包把镜像加载回本地 | docker load -i nginx.tar |
docker build |
按 Dockerfile 构建镜像 | docker build -t docker-demo:1.0 . |
docker push |
把镜像推送到镜像仓库 | docker push registry.demo.com/team/demo:1.0 |
docker tag |
给镜像起别名 / 加仓库前缀 | docker tag nginx:latest registry.demo.com/base/nginx:1.21.6 |
rmi 是 remove image 的缩写,和 Linux 的 rm -i(交互式删除)没有任何关系。别把它记成 rm image。
镜像的四种流向
#mermaid-svg-iqHECqRFAEOp526A{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iqHECqRFAEOp526A .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iqHECqRFAEOp526A .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iqHECqRFAEOp526A .error-icon{fill:#552222;}#mermaid-svg-iqHECqRFAEOp526A .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iqHECqRFAEOp526A .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iqHECqRFAEOp526A .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iqHECqRFAEOp526A .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iqHECqRFAEOp526A .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iqHECqRFAEOp526A .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iqHECqRFAEOp526A .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iqHECqRFAEOp526A .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iqHECqRFAEOp526A .marker.cross{stroke:#333333;}#mermaid-svg-iqHECqRFAEOp526A svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iqHECqRFAEOp526A p{margin:0;}#mermaid-svg-iqHECqRFAEOp526A .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iqHECqRFAEOp526A .cluster-label text{fill:#333;}#mermaid-svg-iqHECqRFAEOp526A .cluster-label span{color:#333;}#mermaid-svg-iqHECqRFAEOp526A .cluster-label span p{background-color:transparent;}#mermaid-svg-iqHECqRFAEOp526A .label text,#mermaid-svg-iqHECqRFAEOp526A span{fill:#333;color:#333;}#mermaid-svg-iqHECqRFAEOp526A .node rect,#mermaid-svg-iqHECqRFAEOp526A .node circle,#mermaid-svg-iqHECqRFAEOp526A .node ellipse,#mermaid-svg-iqHECqRFAEOp526A .node polygon,#mermaid-svg-iqHECqRFAEOp526A .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iqHECqRFAEOp526A .rough-node .label text,#mermaid-svg-iqHECqRFAEOp526A .node .label text,#mermaid-svg-iqHECqRFAEOp526A .image-shape .label,#mermaid-svg-iqHECqRFAEOp526A .icon-shape .label{text-anchor:middle;}#mermaid-svg-iqHECqRFAEOp526A .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iqHECqRFAEOp526A .rough-node .label,#mermaid-svg-iqHECqRFAEOp526A .node .label,#mermaid-svg-iqHECqRFAEOp526A .image-shape .label,#mermaid-svg-iqHECqRFAEOp526A .icon-shape .label{text-align:center;}#mermaid-svg-iqHECqRFAEOp526A .node.clickable{cursor:pointer;}#mermaid-svg-iqHECqRFAEOp526A .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iqHECqRFAEOp526A .arrowheadPath{fill:#333333;}#mermaid-svg-iqHECqRFAEOp526A .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iqHECqRFAEOp526A .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iqHECqRFAEOp526A .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iqHECqRFAEOp526A .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iqHECqRFAEOp526A .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iqHECqRFAEOp526A .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iqHECqRFAEOp526A .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iqHECqRFAEOp526A .cluster text{fill:#333;}#mermaid-svg-iqHECqRFAEOp526A .cluster span{color:#333;}#mermaid-svg-iqHECqRFAEOp526A div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iqHECqRFAEOp526A .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iqHECqRFAEOp526A rect.text{fill:none;stroke-width:0;}#mermaid-svg-iqHECqRFAEOp526A .icon-shape,#mermaid-svg-iqHECqRFAEOp526A .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iqHECqRFAEOp526A .icon-shape p,#mermaid-svg-iqHECqRFAEOp526A .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iqHECqRFAEOp526A .icon-shape .label rect,#mermaid-svg-iqHECqRFAEOp526A .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iqHECqRFAEOp526A .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iqHECqRFAEOp526A .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iqHECqRFAEOp526A :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 本地 Docker
远端
docker pull
docker build
docker save -o
docker load -i
docker push
docker rmi
DockerHub / 私有 Registry
本地镜像仓库
nginx:latest
磁盘文件
nginx.tar
Dockerfile
删除
这张图就是镜像操作的全部。pull/push 走网络,save/load 走文件,build 走源码,rmi 是出清。
为什么要分 save/load 和 push/pull 两套?因为它们解决的是不同场景:push 要求两端都能连仓库,适合 CI/CD;save 不需要网络,适合内网隔离环境、涉密机房、或者客户现场只给 U 盘的情况。
获取镜像:pull 与 build
docker pull
从仓库拉:
bash
# 拉最新版
docker pull nginx
# 拉指定版本,推荐
docker pull nginx:1.21.6
# 从私有仓库拉
docker pull registry.demo.com/base/nginx:1.21.6
拉取过程是一层一层下的,输出里那些 Pull complete 就是分层:
text
1.21.6: Pulling from library/nginx
a2abf6c4d29d: Pull complete
a9edb18cadd1: Pull complete
589b7251471a: Pull complete
Digest: sha256:...
Status: Downloaded newer image for nginx:1.21.6
国内拉 DockerHub 常常卡在几十 KB/s。配镜像加速:
bash
# /etc/docker/daemon.json
{
"registry-mirrors": ["https://<你的加速地址>"]
}
bash
systemctl daemon-reload
systemctl restart docker
docker build
本地构建走 Dockerfile。资料目录里那个 Dockerfile 是完整的 JDK + jar 打包案例:
text
day03-Docker/资料/
├── Dockerfile # 构建脚本
├── docker-demo.jar # 25 MB,待部署的 Java 产物
├── jdk8.tar.gz # 185 MB,JDK 安装包
├── mysql.tar # 377 MB,MySQL 镜像导出包
├── redis.tar.gz # 22 B,空压缩包,不可用
├── nginx.conf # Nginx 配置,配合容器挂载
└── hmy.cnf # MySQL 配置,配合容器挂载
Dockerfile 内容:
dockerfile
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK 的安装目录
ENV JAVA_DIR=/usr/local
# 拷贝 jdk 和 java 项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar
# 安装 JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8
# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin
# 暴露端口
EXPOSE 8090
# 入口,java 项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
构建:
bash
# -t 指定镜像名和 tag,末尾的 . 是构建上下文路径,不是 Dockerfile 路径
docker build -t docker-demo:1.0 .
末尾那个 . 漏掉会报:
text
"docker build" requires exactly 1 argument.
它的含义是"把当前目录作为上下文传给 Docker 守护进程",所以 COPY ./jdk8.tar.gz 才能在上下文里找到文件。如果 Dockerfile 不在当前目录,加 -f:
bash
docker build -f ./docker/Dockerfile -t docker-demo:1.0 .
查看镜像:images
bash
docker images
输出:
text
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 605c77e624dd 4 days ago 141MB
mysql 5.7 c20987f18b13 3 months ago 448MB
每行几个字段的含义:
| 字段 | 说明 |
|---|---|
| REPOSITORY | 镜像仓库名,冒号前的部分 |
| TAG | 版本,没指定就是 latest |
| IMAGE ID | 镜像唯一 ID,SHA256 的前 12 位,删除时可用它指代 |
| CREATED | 镜像构建时间,不是你拉取的时间 |
| SIZE | 解压后占用的磁盘大小,不是 tar 包体积 |
SIZE 显示 141 MB 的 nginx,实际 save 出来的 tar 也差不多这个量级,但如果是多层共享的镜像,docker images 里各镜像 SIZE 加起来会远大于真实占用------底层只读层是被共享的。
常用变体:
bash
docker images # 全部镜像
docker images nginx # 只看 nginx 的各个 tag
docker images -q # 只输出 IMAGE ID,常配合批量删除
docker images -a # 含中间层镜像
docker images --digests # 显示摘要,校验镜像是否被篡改时用
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" # 自定义列
批量清理的经典组合:
bash
# 删除所有 dangling(无 tag 的虚悬镜像),常见于反复 build 同名的产物
docker rmi $(docker images -q -f "dangling=true")
删除镜像:rmi
bash
# 按 名称:tag 删
docker rmi nginx:latest
# 按 IMAGE ID 删,ID 写前几位能唯一识别即可
docker rmi 605c
# 强制删
docker rmi -f nginx:latest
输出:
text
Untagged: nginx:latest
Deleted: sha256:605c77e624dd...
注意这里有两步:Untagged 先摘掉标签,Deleted 才真正删层。如果同一个镜像 ID 挂着多个 tag(比如你 tag 过一次),rmi 只会 Untagged,镜像本体还在。这是很多人"明明删了怎么还在"的原因。
删不掉:镜像被容器占用
这是 rmi 最高频的报错。按容器状态分两种:
text
# 有已停止的容器引用它
Error response from daemon: conflict: unable to delete 605c77e624dd (must be forced) - image is being used by stopped container 8e2a1b3c4d5e
# 有运行中的容器引用它
Error response from daemon: conflict: unable to remove repository reference "nginx:latest" (must force) - container 8e2a1b3c4d5e is using its referenced image
排查顺序:
bash
# 找出哪些容器在用这个镜像,含已停止的
docker ps -a --filter ancestor=nginx:latest
# 先停
docker stop 8e2a1b3c4d5e
# 再删容器
docker rm 8e2a1b3c4d5e
# 最后删镜像
docker rmi nginx:latest
docker rm 删的是容器,docker rmi 删的是镜像,差一个字母,别混。
rmi -f 能强删,但代价是被删镜像对应的容器会变成"孤儿"------容器还在,镜像层没了,重启这个容器必然失败。除非你就是要清磁盘,否则老老实实先删容器。
导出与导入:save 与 load
这一对是离线环境的唯一解法。
save:镜像变文件
先看帮助:
bash
docker save --help
text
Usage: docker save [OPTIONS] IMAGE [IMAGE...]
Save one or more images to a tar archive (streamed to STDOUT by default)
Options:
-o, --output string Write to a file, instead of STDOUT
-o 是 output,指定输出文件。命令格式:
bash
docker save -o [保存的目标文件名称] [镜像名称]
实操:
bash
# 导出 nginx:latest 到当前目录的 nginx.tar
docker save -o nginx.tar nginx:latest
# 一次导出多个镜像到同一个包
docker save -o base.tar nginx:1.21.6 mysql:5.7 redis:6.2
# 不写 -o 则输出到标准输出,配合重定向
docker save nginx:latest > nginx.tar
ls -lh 看一下,文件就出来了:
text
-rw------- 1 root root 141M Sep 29 15:20 nginx.tar
load:文件变镜像
bash
docker load --help
text
Usage: docker load [OPTIONS]
Load an image from a tar archive or STDIN
Options:
-i, --input string Read from tar archive file, instead of STDIN
-q, --quiet Suppress the load output
-i 是 input,读哪个文件;-q 是静默,加了就不打印加载日志。
完整验证流程------先删后加载,确认这个 tar 真的可用:
bash
# 1. 删掉本地镜像
docker rmi nginx:latest
# 2. 确认已经没了
docker images
# 3. 从文件加载回来
docker load -i nginx.tar
# 4. 再看,镜像回来了,tag 也保留着
docker images
load 的输出:
text
Loaded image: nginx:latest
不带 -q 时会看到逐层加载的日志,内网排查的时候别加 -q,看不到进度会以为卡死了。
save 与 export 别混
| 对比项 | docker save / docker load |
docker export / docker import |
|---|---|---|
| 操作对象 | 镜像 | 容器(的运行状态快照) |
| 保留分层历史 | 保留 | 丢弃,压成一层 |
| 保留 CMD / ENTRYPOINT | 保留 | 丢失,要重新指定 |
| 保留环境变量、卷声明 | 保留 | 丢失 |
| 典型用途 | 镜像迁移、离线交付 | 把改过的容器文件系统存成底包 |
结论:迁移镜像一律用 save/load 。export 出来的镜像再 run 会报 no command specified,因为它不知道该跑什么。
离线迁移实操:把 MySQL 搬到内网
资料目录里那个 mysql.tar 377 MB,就是给这种场景准备的。假设客户机房不连外网,交付流程是这样:
内网服务器 移动介质 外网开发机 内网服务器 移动介质 外网开发机 #mermaid-svg-EgdU2LQknCDFzkF0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EgdU2LQknCDFzkF0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EgdU2LQknCDFzkF0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EgdU2LQknCDFzkF0 .error-icon{fill:#552222;}#mermaid-svg-EgdU2LQknCDFzkF0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EgdU2LQknCDFzkF0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EgdU2LQknCDFzkF0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EgdU2LQknCDFzkF0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EgdU2LQknCDFzkF0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EgdU2LQknCDFzkF0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EgdU2LQknCDFzkF0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EgdU2LQknCDFzkF0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EgdU2LQknCDFzkF0 .marker.cross{stroke:#333333;}#mermaid-svg-EgdU2LQknCDFzkF0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EgdU2LQknCDFzkF0 p{margin:0;}#mermaid-svg-EgdU2LQknCDFzkF0 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EgdU2LQknCDFzkF0 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-EgdU2LQknCDFzkF0 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-EgdU2LQknCDFzkF0 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-EgdU2LQknCDFzkF0 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-EgdU2LQknCDFzkF0 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-EgdU2LQknCDFzkF0 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-EgdU2LQknCDFzkF0 .sequenceNumber{fill:white;}#mermaid-svg-EgdU2LQknCDFzkF0 #sequencenumber{fill:#333;}#mermaid-svg-EgdU2LQknCDFzkF0 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-EgdU2LQknCDFzkF0 .messageText{fill:#333;stroke:none;}#mermaid-svg-EgdU2LQknCDFzkF0 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EgdU2LQknCDFzkF0 .labelText,#mermaid-svg-EgdU2LQknCDFzkF0 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-EgdU2LQknCDFzkF0 .loopText,#mermaid-svg-EgdU2LQknCDFzkF0 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-EgdU2LQknCDFzkF0 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-EgdU2LQknCDFzkF0 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-EgdU2LQknCDFzkF0 .noteText,#mermaid-svg-EgdU2LQknCDFzkF0 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-EgdU2LQknCDFzkF0 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EgdU2LQknCDFzkF0 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EgdU2LQknCDFzkF0 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EgdU2LQknCDFzkF0 .actorPopupMenu{position:absolute;}#mermaid-svg-EgdU2LQknCDFzkF0 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-EgdU2LQknCDFzkF0 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EgdU2LQknCDFzkF0 .actor-man circle,#mermaid-svg-EgdU2LQknCDFzkF0 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-EgdU2LQknCDFzkF0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} docker pull mysql:5.7 docker save -o mysql.tar mysql:5.7 拷贝 mysql.tar(377 MB) 上传至 /opt/images/ docker load -i /opt/images/mysql.tar docker images 确认 tag 为 5.7 docker run 启动容器
外网侧:
bash
# 拉
docker pull mysql:5.7
# 导出,注意带上 tag
docker save -o mysql.tar mysql:5.7
# 校验一下文件确实写完整了,377 MB 量级的文件传输中断很常见
ls -lh mysql.tar
sha256sum mysql.tar > mysql.tar.sha256
内网侧:
bash
# 校验完整性
sha256sum -c mysql.tar.sha256
# 加载
docker load -i mysql.tar
# 确认
docker images mysql
启动并挂上资料目录里的配置:
bash
docker run -d \
--name mysql \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123 \
-v /opt/mysql/data:/var/lib/mysql \
-v /opt/mysql/conf:/etc/mysql/conf.d \
mysql:5.7
hmy.cnf 放 /opt/mysql/conf 下:
ini
[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000
两个真实的文件坑
坑一:后缀不代表格式。 资料目录里的 redis.tar.gz 只有 22 字节,file 一下会发现它是个空的 ZIP:
bash
$ file redis.tar.gz
redis.tar.gz: Zip archive data (empty)
load 它只会得到一句干巴巴的报错:
text
open /var/lib/docker/tmp/docker-import-xxx/.dockerenv: not a directory
或者更常见的:
text
Error processing tar file(exit status 1): unexpected EOF
所以拿到包先 ls -lh 看体积,再 file 确认类型,别直接 load。
坑二:.gz 后缀的镜像包未必是 gzip。 docker save 产出的是未压缩的 tar,想省空间得自己压:
bash
# 导出并压缩,377 MB 能压到 100 MB 出头
docker save mysql:5.7 | gzip > mysql.tar.gz
# 加载时解压
gunzip -c mysql.tar.gz | docker load
而资料目录里的 jdk8.tar.gz 是 gzip 压缩的 JDK 安装包(给 Dockerfile 里 tar -xf 用的),不是 Docker 镜像包。两者名字一样,用途完全不同------判断依据是有没有 manifest.json 这个文件:
bash
# 真镜像包里有 manifest.json 和一串层目录
tar -tf mysql.tar | head -5
# mysql.tar 会输出:
# 0e5b0e0f0e0f.../json
# 1a2b3c4d5e6f.../layer.tar
# manifest.json
# repositories
推送镜像:tag 与 push
推之前必须先把镜像名改成带仓库地址的形态,Docker 靠镜像名判断推去哪:
bash
# 语法:docker tag 源镜像 目标镜像
docker tag docker-demo:1.0 registry.demo.com/team/docker-demo:1.0
# 一个镜像可以有多个名字,images 里会重复显示,IMAGE ID 相同
docker images
登录并推送:
bash
docker login registry.demo.com
docker push registry.demo.com/team/docker-demo:1.0
私有仓库走 http 时 Docker 默认拒绝,要在 /etc/docker/daemon.json 加白名单:
json
{
"insecure-registries": ["registry.demo.com:5000"]
}
bash
systemctl daemon-reload
systemctl restart docker
忘了这步会报:
text
http: server gave HTTP response to HTTPS client
push 之前还有一个常见疏漏:没 login。报错长这样:
text
denied: requested access to the resource is denied
忘了命令怎么办:--help
命令记不住是常态,正确的做法是查帮助。这个能力比背命令值钱。
bash
# 看 Docker 全部命令
docker --help
# 看某个命令的详细用法
docker images --help
docker save --help
docker load --help
docker images --help 的输出:
text
Usage: docker images [OPTIONS] [REPOSITORY[:TAG]]
List images
Options:
-a, --all Show all images (default hides intermediate images)
--digests Show digests
-f, --filter filter Filter output based on conditions provided
--format string Pretty-print images using a Go template
--no-trunc Don't truncate output
-q, --quiet Only show image IDs
Usage 那行就是语法模板,方括号是可选项,大写词是占位符。看懂这一行,命令基本就能写对。
实战坑汇总
| 现象 | 报错 | 原因 | 处理 |
|---|---|---|---|
| 镜像名写错 | pull access denied for mysql-5.7, repository does not exist |
冒号写成横杠,被当成另一个仓库名 | 改回 mysql:5.7 |
| tag 写成版本前缀 | manifest for nginx:1.21 not found |
tag 必须完整匹配,没有模糊匹配 | 写全 1.21.6,或去 DockerHub 查真实 tag |
| rmi 删不掉 | image is being used by stopped container xxx |
已停止的容器仍引用该镜像 | docker rm xxx 后再 rmi |
| rmi 删不掉 | container xxx is using its referenced image |
运行中的容器在引用 | docker stop xxx && docker rm xxx |
| rmi 只 Untagged | 输出只有 Untagged: 没有 Deleted: |
同一镜像 ID 挂着多个 tag | 把所有 tag 都 rmi 掉,或用 IMAGE ID 删 |
| 磁盘没释放 | rmi 成功但 df -h 没变小 |
有 dangling 层或容器可写层残留 | docker system df 看占用,docker system prune -a 清理 |
| load 失败 | unexpected EOF |
tar 包传输不完整或是空文件 | ls -lh 核对体积,sha256sum 校验 |
| build 报参数错 | requires exactly 1 argument |
漏了末尾的构建上下文 . |
补上 . |
| push 被拒 | requested access to the resource is denied |
未登录或命名空间无权限 | docker login 后重试 |
| push 协议错 | server gave HTTP response to HTTPS client |
私有仓库是 http | daemon.json 加 insecure-registries |
完整演练:从零走一遍
把上面所有命令串起来,一条龙:
bash
# 1. 拉镜像
docker pull redis:6.2
# 2. 查看
docker images
# 3. 导出
docker save -o redis.tar redis:6.2
ls -lh redis.tar
# 4. 删除
docker rmi redis:6.2
# 5. 确认已删除
docker images
# 6. 加载回来
docker load -i redis.tar
# 7. 再确认
docker images
命令速览
| 命令 | 作用 |
|---|---|
docker pull nginx:1.21.6 |
拉取指定版本镜像 |
docker images |
列出本地镜像 |
docker images -q |
只输出镜像 ID |
docker images -f "dangling=true" |
筛出虚悬镜像 |
docker rmi nginx:latest |
删除镜像 |
docker rmi -f 605c |
按 ID 强制删除 |
docker save -o nginx.tar nginx:latest |
导出镜像为 tar |
| `docker save nginx:latest | gzip > nginx.tar.gz` |
docker load -i nginx.tar |
从 tar 导入镜像 |
docker load -q -i nginx.tar |
静默导入 |
docker build -t demo:1.0 . |
按 Dockerfile 构建 |
docker tag demo:1.0 reg.com/team/demo:1.0 |
打仓库前缀标签 |
docker push reg.com/team/demo:1.0 |
推送到仓库 |
docker save --help |
查命令用法 |
docker system df |
看 Docker 真实磁盘占用 |
官方文档
- docker images 命令参考
- docker save 命令参考
- docker load 命令参考
- docker rmi 命令参考
- docker build 命令参考
- Docker Hub 镜像仓库
总结
- 镜像名
[repository]:[tag],tag 缺省是latest。CI 里写死版本号,latest只留给本地试。 - 获取有两条路:
pull从仓库拿,build从 Dockerfile 造。分发也有两条路:push走网络,save/load走文件------内网隔离环境只有后者可用。 rmi删不掉几乎都是容器在引用。先docker ps -a --filter ancestor=<镜像>找出来,rm容器再rmi镜像,-f强删会留下孤儿容器。save保留分层和 ENTRYPOINT,export只存文件系统快照。迁移镜像只用前者。save出来是未压缩 tar,.gz后缀不代表 gzip;拿到包先ls -lh+file验一遍再load。- 命令不用背,
docker xx --help的 Usage 行就是语法模板。