1. 从“镜像”这个词聊起:为什么Docker镜像如此关键
如果你用过虚拟机,比如VMware或者VirtualBox,那你对“镜像”这个词应该不陌生。一个虚拟机镜像文件(通常是.iso或.ova格式)里,包含了完整的操作系统、预装的软件和配置。拿到这个镜像,你就能在任何支持虚拟化的机器上,启动一个和镜像里一模一样的系统环境。Docker镜像在概念上与此类似,但它的设计哲学和实现方式,却带来了革命性的不同。
简单来说,Docker镜像是一个轻量级、可执行的独立软件包。它包含了运行某个应用所需的一切:代码、运行时环境、系统工具、系统库和设置。但和动辄几个G的虚拟机镜像不同,Docker镜像的精妙之处在于它的分层存储和联合文件系统。你可以把它想象成一个千层蛋糕,或者一个洋葱。最底层是基础层(比如一个精简的Ubuntu系统),每一层都是在上一层的基础上做的修改(比如安装Python,再比如复制你的应用代码)。当你构建一个新镜像时,你只是在已有的层上添加新的薄薄一层,而不是复制整个文件系统。这意味着镜像可以非常小,传输和部署速度极快,并且不同镜像可以共享相同的基础层,极大地节省了磁盘空间。
这解决了开发运维中一个老大难问题:“在我机器上能跑,为什么到你那就挂了?” 因为Docker镜像保证了环境的一致性。你本地构建、测试通过的镜像,可以毫无差别地运行在测试服务器、预生产环境和生产服务器上。这种“一次构建,处处运行”的能力,正是容器技术风靡的核心。所以,学会使用Docker镜像,不仅仅是学会几条命令,更是掌握了一种标准化交付和环境管理的方法论。无论你是开发者、测试还是运维,这都是必须跨过的门槛。
2. 镜像的获取:从拉取到搜索的完整操作链
我们不可能每次都从零开始构建镜像,更多时候是使用社区或官方已经构建好的镜像作为基础。这就涉及到镜像的获取。
2.1 核心命令:docker pull的细节与加速
最基础的命令是docker pull。它的语法是docker pull [OPTIONS] NAME[:TAG|@DIGEST]。
NAME: 镜像名称,通常由三部分组成
[仓库服务器地址/][用户名/]镜像名。如果不指定仓库地址,默认从Docker Hub拉取。ubuntu: 从Docker Hub拉取官方ubuntu镜像。library/ubuntu: 同上,library是Docker Hub官方组织的命名空间。nginx: 拉取官方nginx镜像。bitnami/nginx: 从Docker Hub拉取bitnami用户(组织)下的nginx镜像。registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.2: 从阿里云镜像仓库拉取指定镜像。
TAG: 标签,用于标识镜像的不同版本。如果不指定,默认拉取
latest标签。docker pull nginx:1.21-alpine: 拉取基于Alpine Linux的Nginx 1.21版本。Alpine镜像以体积小著称。docker pull python:3.9-slim: 拉取基于Debian Slim的Python 3.9版本,比完整版更精简。
DIGEST: 镜像摘要,一个唯一的、基于内容的哈希值(如
sha256:abc123...)。标签可以被移动(比如latest今天指向1.0,明天可能指向1.1),但摘要永远指向同一个具体的镜像层,用于生产环境确保绝对一致性。
注意: 直接使用
docker pull nginx拉取的是nginx:latest。在生产环境中,强烈建议永远不要使用latest标签,因为它是一个“移动靶”,今天拉的和明天拉的可能是两个完全不同的版本,会引入不可预知的风险。务必指定具体版本号。
在国内,从Docker Hub拉取镜像速度可能很慢甚至失败。配置镜像加速器是必做操作。以阿里云为例(其他云服务商类似):
- 注册阿里云账号,进入“容器镜像服务” -> “镜像工具” -> “镜像加速器”。
- 你会得到一个专属的加速器地址,如
https://xxxx.mirror.aliyuncs.com。 - 根据你的操作系统(Docker Desktop for Windows/Mac 或 Linux 上的 Docker Daemon)修改配置。对于Linux,通常是修改
/etc/docker/daemon.json文件(没有则创建):{ "registry-mirrors": ["https://xxxx.mirror.aliyuncs.com"] } - 重启Docker服务:
sudo systemctl restart docker。 配置后,docker pull命令会优先从国内镜像源拉取,速度会有质的提升。
2.2 镜像的探索与发现:docker search
当你不知道某个软件是否有官方或好用的Docker镜像时,docker search命令是你的好帮手。docker search [OPTIONS] TERM。
例如,搜索Redis相关镜像:docker search redis。输出会包含镜像名、描述、星数(受欢迎程度)、是否官方(OFFICIAL)和是否自动构建(AUTOMATED)。
这里有几个关键点:
- OFFICIAL [OK]: 这是Docker官方认证的镜像,由上游软件维护者或Docker公司维护,通常是最安全、最推荐的选择。
- 星数(STARS): 类似于GitHub的Star,反映了社区的受欢迎程度,可以作为参考,但不是绝对标准。
- 不要盲目相信高星数: 有些个人维护的镜像可能功能多、星数高,但也可能包含不必要的软件甚至安全隐患。对于生产环境,优先选择
OFFICIAL镜像,其次选择知名组织(如bitnami,linuxserver)维护的镜像。
搜索时也可以使用过滤器,例如只搜索官方镜像:docker search --filter is-official=true redis。
2.3 实操心得:镜像源与版本选择的艺术
- 版本标签的选择: 看到
python:3.9,你可能会直接用它。但我更推荐python:3.9-slim或python:3.9-alpine。slim版本移除了许多非运行必需的文档、包管理器等,体积更小。alpine基于 Alpine Linux,体积极小(通常只有几MB到十几MB),安全性也更高(更小的攻击面)。除非你确定需要镜像里有gcc、curl等完整工具链,否则slim或alpine是更好的起点。这能显著减少镜像体积,加快拉取和部署速度。 - 镜像加速器的陷阱: 配置了加速器后,大部分公开镜像拉取很快。但要注意,加速器通常只缓存 Docker Hub 上的公开镜像。如果你拉取的是私有仓库(如公司内 Harbor)或某些特定第三方仓库的镜像,加速器是不生效的。此时慢是正常的,需要考虑自建代理或使用云服务商的内部网络。
docker pull的“断点续传”:docker pull过程实际上是并行下载多个镜像层。如果中途网络中断,重新执行docker pull命令,它会自动跳过已经完整下载的层,只拉取缺失的层,实现了类似断点续传的功能,非常人性化。
3. 镜像的管理:查看、识别与清理
拉取和构建的镜像会存储在本地。管理好它们,是保持开发环境整洁、节省磁盘空间的关键。
3.1 查看本地镜像列表:docker images/docker image ls
docker images或docker image ls命令会列出本地所有镜像。输出包含:
- REPOSITORY: 镜像仓库源。
- TAG: 标签。
- IMAGE ID: 镜像的唯一ID(摘要的短格式),是镜像的身份标识。
- CREATED: 创建时间。
- SIZE`: 镜像的虚拟大小**。这里有个非常重要的概念:因为镜像是分层的,这个SIZE是当前镜像独占空间与其所有父层共享空间的总和。因此,多个镜像共享同一基础层时,它们的SIZE加起来会远大于实际磁盘占用。
更常用的命令是docker image ls,它是新式命令,语法更一致。可以配合过滤器使用:
docker image ls nginx: 列出所有包含nginx的镜像。docker image ls --filter dangling=true: 列出所有悬虚镜像(没有标签且未被任何容器引用的中间层镜像)。docker image ls --format “table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}”: 自定义格式化输出,更清晰。
3.2 深入镜像:docker image inspect
docker image inspect [IMAGE_ID或REPOSITORY:TAG]是一个强大的诊断工具。它会以JSON格式输出镜像的完整详细信息,包括:
- 完整摘要(Digest)。
- 创建历史(History): 镜像每一层是如何构建的,相当于
Dockerfile的逆向工程,对于调试镜像构建问题非常有用。 - 配置(Config): 包含了镜像的入口点(Entrypoint)、默认命令(Cmd)、环境变量(Env)、工作目录(Workdir)、暴露端口(ExposedPorts)等核心元数据。当你运行容器时,Docker Daemon就是读取这里的配置。
- 层信息(RootFS.Layers): 列出构成该镜像的所有层的摘要ID。
例如,你想知道一个nginx镜像默认的启动命令是什么,可以运行:docker image inspect nginx:alpine | grep -A 5 “Cmd”。这个命令在排查“为什么我运行的容器和预期不一样”时极其有用。
3.3 镜像的标记与删除
标记(Tag):docker image tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]这个命令并不是复制镜像,而是给本地已有的镜像(通过IMAGE ID指向)起一个新的名字(仓库名+标签)。例如,你从阿里云拉了一个镜像,想把它推送到你自己的私有仓库:
docker pull registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.2 docker image tag registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.2 my-private-registry.com/my-project/pause:3.2 docker push my-private-registry.com/my-project/pause:3.2tag操作是瞬间完成的,因为它只创建了一个新的引用。
删除(Remove):docker image rm [OPTIONS] IMAGE [IMAGE...]或docker rmi。 删除镜像时,只有当该镜像的所有标签都被删除,并且没有容器(无论是运行中还是已停止)依赖该镜像的某一层时,该镜像对应的层才会被真正从磁盘上删除。
docker image rm nginx:latest: 删除nginx:latest这个标签。如果该镜像ID还有其他标签(如nginx:1.21),则镜像层不会被删除。docker image rm -f $(docker image ls -q):危险命令!强制删除所有本地镜像。-f可以强制删除正在被容器使用的镜像(但容器本身会变成“无镜像”的悬空状态,不推荐)。docker image prune: 更安全的清理命令。docker image prune -a会删除所有未被任何容器使用的镜像(悬虚镜像+未被引用的镜像),这是定期清理磁盘的推荐做法。可以结合--filter使用,如docker image prune -a --filter “until=24h”删除24小时前创建的未使用镜像。
3.4 实操心得:磁盘空间清理与镜像ID的奥秘
- 真正的磁盘占用查看:
docker system df命令是你的救星。它能清晰展示Docker使用的磁盘空间构成:镜像(Images)、容器(Containers)、本地卷(Local Volumes)和构建缓存(Build Cache)。你会看到每个类别的总大小、活跃大小和可回收空间。当磁盘告急时,首先运行这个命令定位问题。 - 镜像ID冲突的误解: 有时
docker image ls显示两个不同名称的镜像拥有相同的短IMAGE ID。这并不意味着它们是同一个文件的两份拷贝,而是因为它们共享了完全相同的顶层镜像层。IMAGE ID是顶层镜像层内容的哈希值。如果两个镜像基于同一个基础镜像,且最后的操作层内容完全一样(比如都执行了RUN apt-get update),那么它们的IMAGE ID就可能相同。docker image inspect查看完整的RootFS.Layers列表才能看到所有层。 - 删除镜像时的“被引用”错误: 当你尝试删除一个镜像时,如果提示“该镜像正被某个容器使用”,即使这个容器已经停止了(Exited状态),也会阻止删除。你需要先删除依赖它的容器(
docker rm 容器ID),或者使用-f强制删除(不推荐,可能导致容器异常)。更好的习惯是,在停止容器后,如果确定不再需要,立即将其删除。
4. 镜像的构建与定制:编写你的Dockerfile
使用现成镜像只是第一步,真正的威力在于定制自己的镜像。这通过编写Dockerfile来实现。Dockerfile是一个文本文件,包含了一系列构建镜像所需的指令。
4.1 Dockerfile核心指令详解
一个典型的Dockerfile如下所示:
# 1. 指定基础镜像 FROM python:3.9-slim # 2. 设置维护者信息(已弃用,可用LABEL替代) LABEL maintainer="your-email@example.com" # 3. 设置工作目录 WORKDIR /app # 4. 复制当前目录文件到容器内 COPY requirements.txt . # 5. 运行命令安装依赖 RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple # 6. 复制应用代码 COPY . . # 7. 声明运行时容器暴露的端口 EXPOSE 8080 # 8. 定义环境变量 ENV FLASK_APP=app.py ENV FLASK_ENV=production # 9. 指定容器启动时执行的命令 CMD ["flask", "run", "--host=0.0.0.0", "--port=8080"]- FROM:必须是第一条非注释指令。指定构建的基础镜像。选择合适的基础镜像是优化最终镜像体积和安全的起点。
- RUN: 在镜像构建过程中执行命令,并提交结果到新镜像层。常用于安装软件包、编译代码等。每条RUN指令都会创建一个新层,所以尽量将多个命令合并,并用
&&连接,用\换行,最后清理缓存,以减少层数。例如:RUN apt-get update \ && apt-get install -y --no-install-recommends some-package \ && rm -rf /var/lib/apt/lists/* # 清理apt缓存,减小镜像 - COPY vs ADD: 两者都用于复制文件。
COPY是更纯粹的文件复制。ADD在复制的同时,还支持从URL下载和解压tar包。除非你需要ADD的自动解压或下载功能,否则一律使用COPY,因为它的行为更可预测。 - WORKDIR: 设置工作目录。相当于
cd到这个目录,并且后续的RUN,CMD,ENTRYPOINT,COPY,ADD指令都会以此目录为当前目录。建议显式设置,避免使用绝对路径。 - EXPOSE:声明容器运行时监听的端口。这只是一个文档说明和元数据,并不会自动映射端口到宿主机。端口映射需要在
docker run时通过-p参数完成。 - ENV: 设置环境变量。这些变量在构建阶段和容器运行时都可用。对于应用配置(如数据库连接字符串),这是很好的注入方式。
- CMD: 指定容器启动时默认执行的命令。一个Dockerfile中只能有一条
CMD指令,如果有多条则只有最后一条生效。CMD有三种格式:CMD [“executable”, “param1”, “param2”](exec格式,推荐)CMD command param1 param2(shell格式)CMD [“param1”, “param2”](作为ENTRYPOINT的默认参数)
- ENTRYPOINT: 配置容器启动时运行的可执行文件。它和
CMD容易混淆。简单理解:ENTRYPOINT定义了“这个容器是干什么的”(不可被覆盖的命令),而CMD提供了默认参数(可以被docker run后面的命令覆盖)。通常组合使用:ENTRYPOINT [“nginx”, “-g”, “daemon off;”]加上CMD [],或者ENTRYPOINT [“python”]加上CMD [“app.py”]。
4.2 构建镜像:docker build的艺术
编写好Dockerfile后,使用docker build命令构建镜像:
docker build -t my-app:1.0 .-t: 为构建的镜像打标签,格式为name:tag。.:构建上下文路径。这个点.非常重要,它指的是当前目录。Docker Daemon会将这个目录下的所有文件(受.dockerignore影响)打包发送给Docker引擎进行构建。因此,不要在构建上下文中放无关的大文件,否则会拖慢构建速度并增加镜像体积。
构建过程是分层的,Docker会利用缓存。如果某一层及其之前的所有层没有变化,Docker会直接使用缓存,这能极大加快重复构建的速度。但这也意味着,如果你修改了Dockerfile中间的某一行,这一行之后的所有层缓存都会失效。
.dockerignore文件: 类似于.gitignore,用于排除不需要发送到Docker引擎的文件,如本地日志、临时文件、.git目录、node_modules等。一个良好的.dockerignore能显著减少构建上下文大小,提升构建速度和安全。
4.3 实操心得:Dockerfile编写的最佳实践与避坑指南
- 层合并与缓存利用: 将变化频率低的指令放在前面,变化频率高的指令(如复制应用代码
COPY . .)放在后面。例如,先COPY requirements.txt和RUN pip install,再COPY . .。这样当你只修改了应用代码而没改依赖时,依赖安装层可以利用缓存,无需重新下载安装。 - 使用非root用户运行: 默认情况下,容器内的进程以root用户运行,这有安全风险。应该在Dockerfile中创建非root用户并切换。
RUN groupadd -r appuser && useradd -r -g appuser appuser USER appuser WORKDIR /home/appuser COPY --chown=appuser:appuser . . CMD与ENTRYPOINT的“坑”: 如果你使用CMD [“service”, “start”]这样的命令,容器的主进程PID 1就是这个命令。当这个命令执行完毕,容器就会退出。对于像Nginx、MySQL这样的服务,它们的启动命令是前台运行的,所以没问题。但如果你启动的是一个脚本,脚本结束后容器就退出了。此时,需要确保你的启动命令是前台运行的。对于bash脚本,可以在最后加一个tail -f /dev/null或exec "$@"来保持前台进程。更好的方式是理解进程模型,让应用本身作为前台进程运行。- 构建时的网络问题: 在
RUN apt-get install或pip install时,可能会因为网络问题失败。可以在Dockerfile内使用国内镜像源加速,如上面例子中的-i https://pypi.tuna.tsinghua.edu.cn/simple。对于apt,可以替换源列表文件。 - 多阶段构建(Multi-stage builds): 这是构建小而精镜像的终极武器。它允许你在一个Dockerfile中使用多个
FROM指令。每个FROM开始一个新的构建阶段。你可以在一个阶段(构建阶段)使用包含编译工具的大镜像来编译代码,然后在另一个阶段(运行阶段)仅复制编译好的可执行文件到一个非常小的基础镜像(如alpine)中。这样,最终的镜像只包含运行所需的最少内容,体积可能只有原来的十分之一。# 第一阶段:构建 FROM golang:1.19 AS builder WORKDIR /app COPY . . RUN go build -o myapp . # 第二阶段:运行 FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=builder /app/myapp . CMD [“./myapp”]
5. 镜像的存储与分发:仓库与Registry
镜像构建好后,需要存储和分享。Docker Registry是集中存放镜像的仓库服务。最著名的是公共的Docker Hub,也有私有部署的方案如Harbor、GitLab Container Registry等。
5.1 推送镜像到仓库:docker push
首先,你需要用docker login登录到目标仓库(如Docker Hub或私有仓库)。 然后,使用docker image tag给本地镜像打上符合目标仓库规范的标签(registry-host:port/username/image-name:tag)。 最后,使用docker push推送。
docker login myregistry.com:5000 docker tag my-app:1.0 myregistry.com:5000/myteam/my-app:1.0 docker push myregistry.com:5000/myteam/my-app:1.05.2 私有仓库的搭建与使用
对于企业或团队,搭建私有镜像仓库是必须的,用于存储内部应用镜像,保障安全和速度。Harbor是目前最流行的企业级私有Registry解决方案,它提供了权限控制、镜像扫描、漏洞检测、复制等高级功能。
即使不用Harbor,Docker也提供了一个简单的Registry镜像,可以快速搭建一个私有的、基础的仓库:
# 拉取registry镜像 docker pull registry:2 # 运行registry容器 docker run -d -p 5000:5000 --name my-registry -v /my-registry-data:/var/lib/registry registry:2现在,你就有了一个运行在localhost:5000的私有仓库。注意,Docker默认认为非HTTPS的仓库是不安全的。对于本地测试,可以修改Docker Daemon配置(/etc/docker/daemon.json)添加“insecure-registries”: [“myregistry.com:5000”]来允许,但生产环境务必配置HTTPS证书。
5.3 实操心得:镜像标签策略与仓库管理
- 有意义的标签: 不要只用
latest。使用语义化版本(如v1.2.3)、Git提交哈希的前几位(如git-abc1234)、构建时间戳(如build-20231027)的组合。例如myapp:v1.2.3-git-abc1234。这能让你快速、准确地定位任何一个镜像。 - 推送前的检查: 在
docker push之前,先用docker image ls确认标签是否正确,用docker image inspect确认镜像的元数据(如环境变量、入口点)是否符合预期。推错了再删除会很麻烦,尤其是公共仓库。 - 私有仓库的认证: 生产环境的私有仓库一定要配置认证。Harbor提供了完善的用户/项目权限体系。对于简单的Registry,可以通过
htpasswd生成密码文件,并在启动容器时挂载进去。客户端则需要先docker login。在CI/CD流水线中,通常使用机器人账户(Robot Account)的令牌(Token)进行认证。 - 镜像的清理策略: 私有仓库的磁盘空间也是有限的。需要制定镜像保留策略,定期清理旧的、无用的镜像。Harbor有内置的标签保留和垃圾回收策略。对于简单的Registry,可以写定时脚本,调用Registry API来删除早于某个时间或未被引用的镜像。注意:删除镜像是一个需要谨慎操作的动作,务必先确认是否有容器或部署依赖它。
6. 镜像安全与最佳实践扫描
使用第三方镜像,安全是重中之重。一个包含漏洞的基础镜像,会将其风险带入你的整个应用链。
6.1 镜像漏洞扫描
在拉取或使用镜像前,应该对其进行漏洞扫描。许多工具和服务可以提供帮助:
- Docker Scout: Docker官方推出的镜像分析工具(原Snyk集成),能直接通过
docker scout命令对本地镜像进行快速评估,给出基础镜像建议、漏洞报告等。 - Trivy: 一款简单、全面的容器漏洞扫描器,开源且易用。
trivy image my-image:tag即可输出详细的漏洞列表。 - Harbor: 集成了Clair、Trivy等扫描器,可以在镜像推送到仓库时自动扫描,并设置策略阻止包含高危漏洞的镜像被部署。
定期扫描你的基础镜像和应用镜像,并关注CVE(公共漏洞披露)更新,及时升级到修复了漏洞的镜像版本。
6.2 安全最佳实践
- 使用最小化基础镜像: 如前面提到的
-alpine或-slim版本。更小的镜像意味着更小的攻击面。 - 以非root用户运行: 如前文Dockerfile部分所述。
- 定期更新镜像: 不仅更新你自己的应用层,也要定期更新
FROM的基础镜像,以获取安全补丁。可以在CI/CD流水线中设置定时任务,重新构建镜像。 - 不要将机密信息放入镜像: 永远不要在Dockerfile中用
ENV写死密码、密钥。使用Docker的--env-file参数、Docker Compose的env_file配置,或者更专业的密钥管理服务(如Kubernetes Secrets, HashiCorp Vault)在运行时注入。 - 使用多阶段构建: 避免在最终的生产镜像中包含编译工具、源代码等敏感或无用信息。
7. 镜像的导入与导出:离线环境下的搬运工
在某些隔离网络(内网、无外网环境)中,需要将镜像从一个环境迁移到另一个环境。Docker提供了save/load和export/import两对命令,但它们有本质区别。
7.1docker save与docker load:镜像的归档与恢复
这对命令操作的对象是镜像。
docker save -o my-images.tar nginx:alpine redis:alpine: 将一个或多个镜像保存到一个tar归档文件中。这个文件包含了镜像的所有元数据和分层信息。docker load -i my-images.tar: 将tar归档文件加载到本地镜像库中。加载后,通过docker image ls就能看到恢复的镜像。
这是迁移镜像的推荐方式,因为它完整保留了镜像的分层历史和元数据。
7.2docker export与docker import:容器的快照
这对命令操作的对象是容器。
docker export -o my-container.tar container_id: 将一个容器的文件系统导出为一个tar归档文件。注意,它只导出容器的当前文件系统快照,不包含任何历史层、元数据(如环境变量、入口点命令等)。docker import my-container.tar my-image:tag: 从容器快照文件导入成为一个新的镜像。你需要为这个新镜像指定仓库名和标签。导入的镜像丢失了所有构建历史,docker history命令只会显示一条记录。
export/import通常用于制作一个干净的基础文件系统快照,或者调试容器内部状态,不用于常规的镜像迁移。
7.3 实操心得:离线部署的完整流程
假设你需要将开发机上的应用镜像部署到一台无法访问外网的生产服务器。
在开发机(有网)上:
# 1. 构建并标记好镜像 docker build -t my-private-registry.com/myapp:v1.0 . # 2. 将镜像保存为文件 docker save -o myapp-v1.0.tar my-private-registry.com/myapp:v1.0 # 3. 如果需要多个镜像,可以一起保存 docker save -o all-images.tar myapp:v1.0 nginx:alpine postgres:13通过U盘或内部网络,将
myapp-v1.0.tar文件传输到生产服务器。在生产服务器(离线)上:
# 1. 加载镜像文件 docker load -i myapp-v1.0.tar # 2. 验证镜像已加载 docker image ls # 3. 运行容器 docker run -d --name myapp -p 80:8080 my-private-registry.com/myapp:v1.0
这个过程清晰、可靠,是离线环境部署的标准做法。记住关键:迁移镜像用save/load,迁移容器文件系统用export/import,别搞混了。