跳到主要内容
预计阅读 22 分钟

三驾马车:镜像、容器与仓库 —— 搞懂Docker最核心的三个概念

Docker的整个世界,归根结底就围绕着三个东西在转:镜像、容器、仓库。理解了它们的关系,你就掌握了Docker的骨架。

📋 开篇自测:你已经知道多少?

  1. 你能解释镜像和容器之间的关系吗?类比成什么最合适?
  2. 镜像的”分层存储”是怎么回事?它带来了什么好处?
  3. Docker Hub和私有镜像仓库各自适用什么场景?

一、镜像:应用的”冻干标本”

如果你喝过冻干咖啡,你就能理解镜像是什么。

冻干咖啡的制作过程是:先煮好一壶新鲜咖啡,然后通过冷冻干燥技术把水分全部去掉,变成粉末状的咖啡。这个粉末可以长期保存、方便运输,需要喝的时候加热水冲泡就行——还原出来的咖啡和原来那壶味道几乎一样。

Docker镜像就是你的应用程序的”冻干标本”。它把应用的代码、运行环境、依赖库、配置文件等一切运行所需的东西,打包成一个静态的、不可变的文件。这个文件可以存储、传输、分享,需要运行的时候”加水还原”——也就是用它创建一个容器。

镜像的分层结构

镜像有一个精妙的设计:分层存储(Layered Storage)

想象你在叠三明治。最底下一层是面包(基础操作系统,比如Ubuntu),上面放一层生菜(安装Python运行环境),再放一层番茄(安装项目依赖),最上面放一片芝士(复制你的应用代码)。每一层都是独立的,而整个三明治就是你的完整镜像。

这种设计带来了巨大的好处:

共享层:如果你有两个项目都基于Ubuntu + Python,那么Ubuntu层和Python层只需要存储一份。就像两个三明治可以共用同一片面包——当然实际上它们各有各的面包,但在Docker的世界里,相同的层确实只需要下载和存储一次。

增量更新:你修改了应用代码,重新构建镜像时只有代码那一层会变化,其他层直接复用。就像你只换了三明治最上面的芝士,不需要把整个三明治重做。

构建缓存:Docker在构建镜像时会自动缓存每一层。如果某一层的内容没有变化,就直接用缓存,大大加速了构建过程。

我们可以用命令来验证这种分层结构:

# 拉取一个镜像
docker pull nginx

# 查看镜像的分层详情
docker history nginx

你会看到类似这样的输出,每一行就是一层:

IMAGE          CREATED       CREATED BY                                      SIZE
a8758716bb6a   2 weeks ago   CMD ["nginx" "-g" "daemon off;"]                0B
<missing>      2 weeks ago   STOPSIGNAL SIGQUIT                              0B
<missing>      2 weeks ago   EXPOSE map[80/tcp:{}]                           0B
<missing>      2 weeks ago   COPY file:xxx in /docker-entrypoint.d           4.62kB
<missing>      2 weeks ago   COPY file:xxx in /docker-entrypoint.d           3.02kB
<missing>      2 weeks ago   /bin/sh -c set -x     && groupadd...            112MB
<missing>      2 weeks ago   /bin/sh -c #(nop)  ENV NJS_VERSION=0.8.x        0B
...

镜像的命名规则

镜像的名称遵循一套标准的命名规范,理解它有助于你在使用中不犯迷糊:

[仓库地址/]仓库名[:标签]

举几个例子:

完整名称仓库地址仓库名标签
nginxDocker Hub(默认)nginxlatest(默认)
nginx:1.28Docker Hubnginx1.28
ubuntu:22.04Docker Hububuntu22.04
mycompany/webapp:v2Docker Hubmycompany/webappv2
registry.cn-hangzhou.aliyuncs.com/myns/myapp:1.0阿里云仓库myns/myapp1.0

标签(Tag) 是一个非常重要的概念。同一个软件可以有多个版本,每个版本对应不同的标签。如果你不指定标签,默认就是 latest,但请注意——latest 不一定是最新版本!它只是一个约定俗成的默认标签,实际指向哪个版本取决于镜像维护者。

在生产环境中,永远不要用 latest 标签。因为它可能在你不知情的情况下被更新为新版本,导致你的应用行为发生变化。应该始终使用具体的版本标签,比如 nginx:1.28.3

常用镜像操作

# 搜索镜像
docker search redis

# 拉取镜像(默认从Docker Hub)
docker pull redis:7.2

# 查看本地所有镜像
docker images

# 查看镜像详细信息
docker inspect nginx

# 给镜像打标签(相当于起别名)
docker tag nginx:latest my-nginx:v1

# 删除镜像
docker rmi nginx:1.28

# 导出镜像为文件(方便离线传输)
docker save -o nginx.tar nginx:latest

# 从文件导入镜像
docker load -i nginx.tar

🤔 想一想 为什么Docker选择了分层存储而不是把整个镜像做成一个单一的大文件?如果是单一文件,会有哪些问题?


二、容器:镜像的”活体分身”

如果镜像是冻干咖啡粉,那容器就是你冲出来的那杯咖啡——热气腾腾的、活生生的、有生命周期的。

更精确地说:容器是镜像的一个运行实例。从同一个镜像可以创建出任意多个容器,就像同一个菜谱可以做出无数份同样的菜。每个容器都是独立的,在一个容器里的修改不会影响其他容器,也不会影响原始镜像。

容器的生命周期

一个容器从生到死,会经历以下几个状态:

创建(Created) → 运行(Running) → 暂停(Paused) → 停止(Stopped) → 删除(Removed)

就像一个人的一天:起床(创建/启动)→ 工作(运行)→ 午休(暂停)→ 下班回家(停止)→ 第二天换了个人来上班(删除旧容器、创建新容器)。

对应的命令:

# 创建并启动容器
docker run -d --name my-redis redis:7.2

# 暂停容器(进程被冻结,但不释放资源)
docker pause my-redis

# 恢复容器
docker unpause my-redis

# 停止容器(发送SIGTERM信号,等待优雅退出)
docker stop my-redis

# 强制停止(发送SIGKILL信号,立即终止)
docker kill my-redis

# 重新启动已停止的容器
docker start my-redis

# 删除容器(必须先停止)
docker rm my-redis

# 强制删除运行中的容器
docker rm -f my-redis

容器的可写层

这里有一个关键的技术细节需要理解:容器在镜像的只读层之上添加了一个可写层

还是用三明治来比喻。镜像是一个做好的三明治(只读的,不能修改),容器启动时相当于在这个三明治最上面加了一层保鲜膜。你在容器里做的所有修改——创建文件、安装软件、修改配置——都写在这层保鲜膜上。原始的三明治一点都没被动过。

这意味着:

  • 删除容器 = 扔掉保鲜膜。你在容器里做的所有修改都消失了。
  • 原始镜像不受影响。你可以继续用它创建新的容器。
  • 如果你想保留修改,需要用 docker commit 把当前容器的状态保存为一个新镜像(但这不是推荐做法,后面会讲更好的方式)。
# 把容器的当前状态保存为新镜像
docker commit my-container my-new-image:v1

与运行中的容器交互

容器跑起来之后,你经常需要”进去看看”:

# 进入容器的交互式终端
docker exec -it my-redis bash

# 如果容器里没有bash,可以试试sh
docker exec -it my-redis sh

# 在容器里执行单条命令(不进入交互模式)
docker exec my-redis redis-cli ping

# 查看容器日志
docker logs my-redis

# 实时追踪日志(类似tail -f)
docker logs -f my-redis

# 查看容器资源使用情况
docker stats my-redis

# 查看容器内的进程列表
docker top my-redis

# 把容器内的文件复制到宿主机
docker cp my-redis:/etc/redis/redis.conf ./redis.conf

# 把宿主机文件复制进容器
docker cp ./my-config.conf my-redis:/etc/redis/redis.conf

⚠️ 常见误区

  • 误区一:“容器应该像虚拟机一样长期运行”。容器的设计哲学是”用完即弃”。与其修修补补一个旧容器,不如直接用新镜像创建一个新的。
  • 误区二:“docker commit是构建镜像的好方法”。不是。commit产生的镜像缺乏可重复性和透明度。应该用Dockerfile来构建镜像(第4章会详细讲)。
  • 误区三:“一个容器里应该跑多个服务”。每个容器应该只运行一个进程(或者说一个关注点)。需要多个服务协作时,应该用多个容器配合(第7章会讲Docker Compose)。

三、仓库:镜像的”应用商店”

你的手机上有App Store或Google Play用来下载App。Docker世界里也有类似的东西,叫做镜像仓库(Registry)

Docker Hub:公共大集市

Docker Hub(https://hub.docker.com)是Docker官方运营的公共镜像仓库,也是全世界最大的容器镜像集散地。你能在上面找到几乎所有主流开源软件的官方镜像:

  • 操作系统:ubuntu, alpine, debian, centos
  • 编程语言:python, node, golang, openjdk
  • 数据库:mysql, postgres, redis, mongodb
  • Web服务器:nginx, httpd, caddy
  • 消息队列:rabbitmq, kafka

Docker Hub上的镜像分为两种:

官方镜像(Official Images):由Docker或软件官方团队维护,质量有保证,名称前面没有斜杠(如 nginx, python, redis)。在搜索结果里会有”Official”徽章。

社区镜像(Community Images):由个人或第三方组织上传,名称格式为 用户名/镜像名(如 bitnami/redis, linuxserver/nginx)。质量参差不齐,使用前建议检查下载量和更新频率。

# 在命令行搜索镜像
docker search nginx

# 拉取官方Nginx镜像
docker pull nginx

# 拉取某个社区版本的镜像
docker pull bitnami/nginx

私有仓库:自家的保险柜

企业内部的代码和镜像不适合放在公共平台上。这时候就需要搭建私有镜像仓库。

最简单的方案是Docker官方提供的Registry镜像,一条命令就能跑起来一个私有仓库:

# 启动一个本地私有仓库
docker run -d -p 5000:5000 --name my-registry registry:2

# 给镜像打上私有仓库的标签
docker tag nginx:latest localhost:5000/my-nginx:v1

# 推送到私有仓库
docker push localhost:5000/my-nginx:v1

# 从私有仓库拉取
docker pull localhost:5000/my-nginx:v1

在生产环境中,更常用的私有仓库方案有:

  • Harbor:VMware开源的企业级仓库,支持安全扫描、访问控制、镜像签名等高级功能
  • AWS ECR:亚马逊云的容器镜像服务
  • 阿里云容器镜像服务:国内常用的选择
  • GitHub Container Registry:与GitHub深度集成

镜像加速

如果你在国内,直接从Docker Hub拉取镜像可能会很慢甚至超时。配置镜像加速器可以显著提升下载速度。

在Docker Desktop中:打开Settings → Docker Engine,在JSON配置中添加可用的镜像源地址:

{
  "registry-mirrors": [
    "https://你的镜像加速地址"
  ]
}

在Linux中,编辑 /etc/docker/daemon.json

{
  "registry-mirrors": [
    "https://你的镜像加速地址"
  ]
}

然后重启Docker:

sudo systemctl daemon-reload
sudo systemctl restart docker

💡 提示:国内 Docker Hub 镜像加速服务变动频繁,过去常用的阿里云、腾讯云、网易等公共加速地址可能已停止服务或限制访问。建议搜索”Docker 国内镜像加速 + 当前年份”获取最新可用的镜像源列表。部分云服务商仍为自家云主机用户提供内网加速服务,可查阅对应云服务商的文档。

🤔 想一想 如果你是一家初创公司的技术负责人,你会选择Docker Hub的付费方案还是自建私有仓库?两种方案的成本和维护负担分别是什么?


四、三者的关系:一图胜千言

让我们用一张关系图把三个核心概念串起来:

                    ┌─────────────┐
                    │   仓库       │
                    │ (Registry)  │
                    └──────┬──────┘
                      push↑ ↓pull
                    ┌──────┴──────┐
                    │   镜像       │
                    │  (Image)    │
                    └──────┬──────┘
                      run  ↓  ↑ commit
              ┌────────────┼────────────┐
              ↓            ↓            ↓
        ┌──────────┐ ┌──────────┐ ┌──────────┐
        │  容器 A   │ │  容器 B   │ │  容器 C   │
        │(Container)│ │(Container)│ │(Container)│
        └──────────┘ └──────────┘ └──────────┘

用一句话概括它们的关系:仓库是存放镜像的地方,镜像是创建容器的模板,容器是镜像运行起来的实例

再用一个日常类比:仓库就像书店,镜像就像一本书的模板(母版),容器就像你买回家的那本书。书店里有各种各样的书(仓库里有各种镜像),你选了一本买回家(pull镜像),然后可以在上面做笔记、贴便签(容器里的可写层)。即使你把自己那本书写满了笔记,书店里的原版不受影响。

动手验证

让我们通过一系列命令来亲眼看看三者的互动:

# 从仓库拉取镜像
docker pull alpine:3.19

# 查看本地镜像列表
docker images

# 从镜像创建并运行3个独立的容器
docker run -d --name box1 alpine:3.19 sleep 3600
docker run -d --name box2 alpine:3.19 sleep 3600
docker run -d --name box3 alpine:3.19 sleep 3600

# 查看运行中的容器
docker ps

# 在box1里创建一个文件
docker exec box1 sh -c "echo 'hello from box1' > /tmp/greeting.txt"

# 验证box2里没有这个文件
docker exec box2 sh -c "cat /tmp/greeting.txt"
# 输出:cat: can't open '/tmp/greeting.txt': No such file or directory

# 清理
docker rm -f box1 box2 box3

这个实验直观地证明了:从同一个镜像创建的多个容器是完全独立的,在一个容器里做的修改不会影响到其他容器。


📝 掌握度自测

  1. 用自己的话解释什么是Docker镜像的”分层存储”,以及它带来了哪些好处?

  2. docker rundocker start 有什么区别?docker stopdocker kill 又有什么区别?

  3. 为什么不推荐在生产环境中使用 latest 标签?应该怎么做?

  4. 容器的”可写层”是什么?删除容器后,可写层里的数据还在吗?如何持久化保存数据?(可以先思考,答案在第6章)

  5. 简述Docker Hub上”官方镜像”和”社区镜像”的区别,以及你在选择社区镜像时会关注哪些因素。

💡 自我评估

  • 全部答对:你已经牢牢掌握了Docker的三大核心概念!接下来可以进入进阶篇,学习如何构建自己的镜像了。
  • 答对3-4题:对核心概念的理解已经相当不错。建议动手多跑几遍本章的命令示例。
  • 答对1-2题:三驾马车的关系图值得多看几遍。建议回到”三者的关系”部分,结合动手实验加深理解。

购买课程解锁全部内容

告别「在我电脑上能跑」:Docker 容器化实战

¥29.90