第2章 Docker 基础概念

聊 Docker,绕不开三个核心概念:镜像、容器、仓库。外加一个架构图。嗯,说实话,我刚接触 Docker 时最晕的就是这几个词——长得像,又不完全一样。我习惯用「菜谱、菜、菜市场」来打比方,你想想看,是不是好记很多?

2.1 镜像(Image)—— 菜谱

镜像是一个只读模板。它打包了程序运行所需的一切:

  • 操作系统底层(比如 Alpine、Ubuntu)
  • 运行时环境(Python 3.11、Node 18)
  • 应用代码和配置文件
  • 环境变量、端口声明、挂载点

我个人习惯用 docker images 查看本地已有的镜像。你在项目初期可以先从 Docker Hub 拉一个现成的:

docker pull python:3.11-slim

这条命令会从 Docker Hub 把 Python 3.11 的精简版镜像拽到本地。镜像由一层一层可叠加的只读层组成,每一层都是一个文件系统的变更记录。这个分层机制是 Docker 的精髓——复用层,节省空间,加速构建。

小技巧:我曾在一次构建中反复装同一个依赖,后来才发现用多阶段构建能让最终镜像从 1.2GB 瘦到 180MB。别小看这几行 FROM --stage,生产环境里能省不少磁盘和带宽。

镜像的名称格式一般是:用户名/镜像名:标签。标签默认 latest,但我建议你永远用确定版本号,比如 python:3.11-slim 而不是 python:latest——为什么?因为 latest 会随着官方更新变化,今天拉的和下周拉的可能不是同一个东西。避坑啊。

2.2 容器(Container)—— 菜

容器是镜像的运行实例。你按照菜谱做好一道菜,端上桌的就是容器。每个容器都是一个隔离的进程,拥有自己的:

  • 文件系统(基于镜像层 + 可写层)
  • 网络栈(独立 IP 和端口映射)
  • 进程空间(pid 隔离)
  • 用户组(uid/gid 隔离)

启动一个容器很简单:

docker run -d --name my-app -p 8080:80 nginx:alpine

这条命令:后台运行一个容器,名字叫 my-app,把宿主机 8080 端口映射到容器内 80 端口,基于 nginx:alpine 镜像。

我曾经踩过的坑:忘记加 --rm 参数,测试容器用完不删,结果一周下来宿主机磁盘爆满。现在只要不是持久化场景,我都会加 --rm 让容器退出时自动清理。

容器和镜像的核心区别看下表:

维度 镜像 容器
生命周期 持久存储(可删除) 临时实例(可创建/停止/删除)
读写状态 只读 可写(可写层)
命名 镜像名:标签 容器名(或随机ID)
体积 较大(分层复用) 启动后增加约几KB(可写层)
使用场景 构建、分发 运行、调试

2.3 仓库(Registry)—— 菜市场

仓库用来存储和分发镜像。Docker Hub 是最大的公共仓库,但企业环境一般用私有仓库,比如 Harbor、GitLab Container Registry、AWS ECR。

推送一个镜像到仓库:

docker tag my-image:latest my-registry.com/library/my-image:v1.0
docker push my-registry.com/library/my-image:v1.0

拉取命令刚好相反。你想想看,整个 CI/CD 流程里,仓库就像中间件——源码编译成镜像,推送到仓库,再部署到生产节点。我在项目中就用 Harbor 搭建私有仓库,做好镜像扫描和访问控制,安全又高效。

建议:团队里统一标签规范。我用的是 <应用名>:<版本号>-<构建号>,比如 drug-graph:1.2.3-b20250315,一眼能看出哪个版本、哪次构建。

2.4 Docker 架构

Docker 采用 C/S(客户端/服务端)架构。三个主要角色:

  • Docker Client:你敲 docker 命令的终端,本质是一个 CLI 工具。
  • Docker Daemon:后台守护进程,负责管理镜像、容器、网络、卷等。通常叫 dockerd。
  • Docker Registry:镜像仓库,Client 和 Daemon 都能与之交互推拉镜像。

整个交互流程我用一张图表示:

Docker 架构流程 Docker Client Docker Daemon Registry REST API push/pull 容器 网络 卷 说明 Client 通过 REST API 告诉 Daemon 做什么;Daemon 负责管理本地的镜像、容器、网络和卷;如果需要新镜像,Daemon 会去 Registry 拉取。
一句话总结:你敲命令 -> Docker Client 转发给 Daemon -> Daemon 拉镜像、启动容器 -> 容器与宿主机通过命名空间隔离运行。整个过程在用户态和内核态协作完成,但用户只需要关心前两步。

底层技术主要依赖 Linux 内核的 NameSpaces(实现资源隔离)和 Cgroups(实现资源限制)。我刚开始学的时候觉得这是黑魔法,后来亲手写了几个 pid namespace 的小 demo 才真正理解。不过你现在不需要深挖内核,先用好 Docker 命令,后续有专门的章节讲安全与资源控制。

特别提醒:Docker Desktop 在 macOS/Windows 上默认会创建一台 Linux 虚拟机来跑 Daemon。如果你在生产环境,绝对不要用 Docker Desktop,必须上原生的 Linux 系统。我曾经见过同事在 macOS 上压测,结果性能折损 30%,排查了半天才发现是虚拟机逃逸的 overhead。

好,三个概念都讲完了。我来用一个实际的小实验帮你串联起来:

  1. 拉一个 hello-world 镜像:docker pull hello-world
  2. 查看本地镜像:docker images
  3. 启动容器:docker run hello-world
  4. 查看容器列表(包括刚停止的):docker ps -a
  5. 删除容器:docker rm <容器ID>
  6. 删除镜像:docker rmi hello-world

自己去试试看。你想想看,这几条命令走完,镜像、容器、仓库的关系是不是就清晰了?后面我们就把这些基础运用到药物图谱的 Docker 部署上,嗯,那才是真正有意思的部分。