2. Docker核心概念:镜像、容器、仓库的关系与区别

刚接触Docker那会儿,我也跟你一样蒙圈。镜像、容器、仓库——这三个词天天挂在嘴边,但真要让我说清楚它们到底什么关系……嗯,我琢磨了好一阵子。

后来在项目里踩过坑、翻过车,才慢慢摸出门道。说白了,这三个东西就像「菜谱、菜品、菜市场」。我习惯用这个比喻来跟团队新人解释,今天也跟你聊聊。

2.1 镜像(Image)——只读的模板

镜像是什么?一个静态的、只读的文件。它包含了运行某个应用所需的全部环境——操作系统层、依赖库、配置文件、启动脚本,一样不落。

你可以把它想象成 Java 里的类(Class),或者虚拟机里的快照。但有个关键区别:镜像是分层的。

我曾经遇到一个典型案例:项目的 Python 依赖有 50 多个包,每次构建都从零下载,慢得让人崩溃。后来我改了 Dockerfile,把稳定的依赖放前面、经常改的放后面,利用镜像层缓存,构建时间从 8 分钟降到了 40 秒。

个人小习惯:写 Dockerfile 时,我会把 apt-get update 和 pip install 拆成多层,每层只变一个东西。这样改动小层时前面的大层缓存还能用。

镜像的关键特性:

  • 只读——启动后不能修改镜像本身
  • 分层——每一层只记录差异,复用效率高
  • 可复用——同一镜像可以启动无数个容器
  • 可标记——用 image:tag 区分版本

看看你本地有哪些镜像:

docker images
# 或者
docker image ls

列出来的每一行就是一个镜像。注意那个 IMAGE ID,它是内容的哈希值,不是随机的——同样的内容永远生成同样的 ID。

2.2 容器(Container)——运行中的实例

容器就是「镜像+可写层」运行起来的东西。你想想,镜像只读,那怎么改配置文件?怎么写日志?容器在镜像顶层加了一个可写层(也叫容器层),所有变更都写在这一层。

容器和镜像的关系,就是对象和类的关系。一个镜像可以同时跑出几十个容器,彼此隔离,互相不干扰。

我记得有一次线上排查,有个容器日志爆了,磁盘打满。我用 docker exec 进去看了下,发现是某个程序在循环写调试日志。因为容器层是可写的,所以日志一直往上堆。后来我在启动时加了日志大小限制,才彻底解决。

容器的生命周期:

  1. 创建(docker create)——拿到一个容器 ID,但没运行
  2. 启动(docker start)——运行起来
  3. 运行中——处理请求,写日志
  4. 停止(docker stop)——进程退出,可写层还在
  5. 删除(docker rm)——全部清空

避坑指南:我曾经在开发环境里直接 docker run 了 20 个容器做压力测试,跑完忘了删。第二天发现 /var/lib/docker 用掉了 80GB。后来我养成了习惯:临时容器加 --rm 参数,退出自动删除。

2.3 仓库(Registry)——镜像的存储与分发

仓库就是存镜像的地方。本地镜像只能自己在单机用,要分享给其他机器、其他团队,就得推送到仓库。

官方仓库是 Docker Hub,很多公司也会搭私有仓库(比如 Harbor、Nexus、AWS ECR)。

常用命令:

# 从仓库拉取
docker pull nginx:1.25-alpine

# 推送到仓库
docker push myregistry.example.com/myapp:v2.0

# 搜索镜像(不推荐线上用,耗时长)
docker search python

仓库里每个镜像通过 repo/name:tag 定位。比如 library/nginx:1.25-alpine,library 是命名空间(官方镜像的命名空间就叫 library),nginx 是镜像名,1.25-alpine 是标签。

2.4 三者的关系——一张图讲清楚

下面这张图是我给团队培训时画的精简版。看完你就明白它们怎么连在一起了。

镜像 · 容器 · 仓库 关系图 仓库 Registry 存储与分发镜像 pull / push 镜像 Image 只读模板 · 分层存储 docker run / create 容器 A 可写层 + 只读层 容器 B 可写层 + 只读层 一个镜像可以启动多个容器,容器之间共享相同的只读层,各自拥有独立可写层

2.5 关键区别对照表

我整理了一个表格,把三者的核心维度说清楚。你在脑子里过一遍,基本就不会再搞混了。

维度 镜像 容器 仓库
本质 静态只读文件 运行中的进程 存储系统
是否可写 否(只读) 是(可写层) 是(可存储、删除)
生命周期 持久(删了才没) 临时(停止可保留) 持久(除非手动删)
标识方式 name:tag + Image ID Container ID + name registry/name:tag
常见操作 build, pull, push, tag run, stop, rm, exec login, push, pull
类比 菜谱(印好的) 按菜谱炒出的菜 菜市场(买/卖菜谱)

2.6 动手实践:体验三者的流转

光说不练假把式。跟着走一遍,你就能感受到三者在手底下的感觉。

  1. 从仓库拉一个镜像

    docker pull alpine:3.19
  2. 查看本地镜像列表——你会发现仓库里的东西跑到你本地了。

    docker images
  3. 根据镜像启动一个容器

    docker run --name myalpine -it alpine:3.19 /bin/sh

    这会直接进到容器 shell。在里面敲 cat /etc/os-release 看看系统版本。

  4. 退出但不删除容器:按 Ctrl+D 或打 exit。容器会停止,但可写层还在。

  5. 重启容器并看看变化

    docker start -ai myalpine

    你之前对文件系统做的修改还在,因为可写层没有丢。

  6. 用容器创建一个新镜像(了解即可,生产不常用)

    docker commit myalpine mycustom:test

    这会把容器的可写层+原始镜像层打包成一个新镜像。

我的一点体会:刚入门时总以为镜像和容器差不多,直到我发现同样一个镜像启动的不同容器,在里面装不同的包、改不同的配置,彼此完全独立。那一刻我才真正理解了「隔离」的含义。而且你还记得吗?容器删了没关系,镜像还在,可以再起来一个全新的。

2.7 避坑总结

我把常见的坑列出来,你记一下,能省不少时间:

  • 别把容器当虚拟机玩——容器里不要跑 systemd,不要装 sshd,每个容器只跑一个进程才是正解。
  • 镜像标签别用 latest 打生产环境——latest 是动态的,今天拉和明天拉可能不同。我习惯用语义版本号 + Git commit 短哈希。
  • 仓库一定要配访问控制——别让你的私有镜像流到公网上。我曾经见过有人把生产环境的数据库密码直接写进 Dockerfile,然后又推到公开仓库……后果你自己想。
  • 容器数据要持久化必须用 volume——容器一删,可写层就没了。重要数据一定要挂载到宿主机或 volume 里。