Docker到底是什么?一文读懂镜像、容器、仓库三大核心概念
引言:从一个真实场景说起
你有没有遇到过这样的场景?
周五下午,同事把代码提交到 Git,留下一句“我本地跑得好好的”,然后潇洒下班。周一你拉下代码,npm install 报错、Python 版本不对、MySQL 连不上、Redis 没启动……折腾一整天,项目还是跑不起来。
这个经典的“环境不一致”问题,几乎每个开发者都经历过。而 Docker 正是为了解决它而生的:把应用和它依赖的环境一起打包,做到“一次构建,到处运行”。
但很多初学者在刚接触 Docker 时,会被“镜像、容器、仓库”这三个词绕晕:它们到底有什么区别?为什么拉取的是镜像,运行的却是容器?这篇文章就带你一次性把它们彻底搞清楚。
一、先建立整体认知:Docker 是什么
Docker 是一个开源的容器化平台,它允许开发者将应用及其依赖打包到一个轻量级、可移植的“容器”中,然后发布到任何支持 Docker 的机器上运行。
如果用一句话概括 Docker 的核心价值:
Docker = 标准化的打包 + 标准化的分发 + 标准化的运行
而支撑这套体系的三块基石,就是我们今天要讲的主角:
| 镜像 | Image | 类 / 安装包 / 菜谱 | 只读模板,定义应用和运行环境 |
| 容器 | Container | 对象 / 运行中的程序 / 菜肴 | 镜像的运行实例,可读写、可启停 |
| 仓库 | Registry/Repository | GitHub / 应用商店 | 存放和分发镜像的地方 |
我们先通过一张关系图建立整体印象:
Dockerfile –build–> 镜像(Image) –run–> 容器(Container)
|
push/pull
|
仓库(Registry)
接下来逐一拆解。
二、镜像(Image):容器的“模板”与“菜谱”
2.1 什么是镜像
镜像是一个只读的、包含应用及其运行环境的模板。它由一层一层的文件系统叠加而成(UnionFS),每一层代表一次构建操作。
你可以把镜像理解成“做菜的菜谱”:菜谱本身不能吃,但按照它就能做出一道菜。同样,镜像本身不能运行,但按照它就能启动一个容器。
一个镜像通常包含:
- 基础操作系统(如 ubuntu:22.04、alpine:3.19)
- 运行时环境(如 JDK、Python、Node.js)
- 应用代码或二进制文件
- 启动配置(如 CMD、ENTRYPOINT、环境变量)
2.2 镜像的命名规范
镜像名遵循如下格式:
[registry地址/][命名空间/]仓库名[:标签]
| registry地址 | 否 | 默认 Docker Hub | docker.io、harbor.example.com |
| 命名空间 | 否 | 默认 library | library、bitnami |
| 仓库名 | 是 | 镜像名称 | nginx、mysql |
| 标签 | 否 | 默认 latest | 1.25、8.0、alpine |
例如:
nginx # 等价于 docker.io/library/nginx:latest
mysql:8.0 # Docker Hub 官方 MySQL 8.0
bitnami/redis:7.2 # bitnami 命名空间下的 Redis
harbor.example.com/app/api:v1.0.0 # 私有仓库镜像
注意:生产环境绝对不要使用 latest 标签,因为它会随时间变化,导致“同样的命令拉到的镜像却不一样”,难以复现问题。
2.3 镜像常用命令
(1)拉取镜像:docker pull
docker pull nginx:1.25
预期输出:
1.25: Pulling from library/nginx
a2abf6c4d29d: Pull complete
…
Digest: sha256:xxxxxxxx
Status: Downloaded newer image for nginx:1.25
docker.io/library/nginx:1.25
(2)查看本地镜像:docker images
docker images
预期输出:
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx 1.25 a8758716bb6a 2 weeks ago 187MB
mysql 8.0 b1c2d3e4f5a6 3 weeks ago 586MB
常用参数:
| -a | 显示所有镜像(含中间层) |
| -q | 只显示 IMAGE ID |
| –filter | 过滤,如 –filter "dangling=true" |
| –format | 自定义输出格式 |
(3)查看镜像详情:docker inspect
docker inspect nginx:1.25
输出为 JSON,包含镜像的架构、环境变量、暴露端口、层信息等。可以用 –format 提取关键信息:
docker inspect -f '{{.Architecture}} {{.Os}}' nginx:1.25
# 输出:amd64 linux
(4)删除镜像:docker rmi
docker rmi nginx:1.25
如果镜像正在被容器使用,需要先删除容器,或使用 -f 强制删除:
docker rmi -f nginx:1.25
(5)构建镜像:docker build
docker build -t myapp:v1.0 .
其中 . 表示构建上下文为当前目录,Docker 会读取当前目录下的 Dockerfile。
一个最简单的 Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install –production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
2.4 镜像的分层机制(重点)
镜像由多个只读层叠加而成,每一层对应 Dockerfile 中的一条指令。例如:
FROM ubuntu:22.04 # 第 1 层
RUN apt-get update # 第 2 层
RUN apt-get install -y curl # 第 3 层
COPY app.py /app/ # 第 4 层
用 docker history 查看:
docker history myapp:v1.0
分层的好处:
最佳实践:把不常变化的指令放在前面(如安装依赖),把常变化的放在后面(如 COPY 代码),最大化利用构建缓存。
三、容器(Container):镜像的“运行实例”
3.1 什么是容器
容器是镜像运行时的实例,是一个独立、隔离的进程环境。它拥有自己的文件系统、网络、进程空间,但共享宿主机的内核。
用前面的比喻:如果镜像是“菜谱”,那么容器就是“按菜谱做出来的菜”。一道菜谱可以做出很多盘菜,一个镜像也可以启动很多个容器。
容器与镜像的关键区别:
| 可读写 | 只读 | 可读写(在镜像之上加一层可写层) |
| 是否运行 | 否 | 是(有进程) |
| 生命周期 | 长期存在 | 可创建、启动、停止、删除 |
| 存储 | 分层只读 | 镜像层 + 容器可写层 |
3.2 容器常用命令
(1)运行容器:docker run
docker run -d –name my-nginx -p 8080:80 nginx:1.25
参数说明:
| -d | 后台运行(detach) |
| –name | 指定容器名 |
| -p 8080:80 | 端口映射,宿主机 8080 → 容器 80 |
| -v | 挂载数据卷 |
| -e | 设置环境变量 |
| –rm | 容器退出后自动删除 |
| -it | 交互式终端 |
访问验证:
curl http://localhost:8080
预期输出(Nginx 欢迎页 HTML 片段):
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
…
(2)查看容器:docker ps
docker ps # 查看运行中的容器
docker ps -a # 查看所有容器(含已停止)
预期输出:
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
f3a1b2c4d5e6 nginx:1.25 "/docker-entrypoint.…" Up 2 minutes 0.0.0.0:8080->80/tcp my-nginx
(3)进入容器:docker exec
docker exec -it my-nginx /bin/bash
进入后可以查看容器内部环境:
root@f3a1b2c4d5e6:/# cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
(4)查看日志:docker logs
docker logs -f –tail 100 my-nginx
| -f | 持续跟踪输出 |
| –tail N | 只显示最后 N 行 |
| -t | 显示时间戳 |
(5)停止与删除容器
docker stop my-nginx # 停止
docker start my-nginx # 启动已停止的容器
docker restart my-nginx # 重启
docker rm my-nginx # 删除(需先停止)
docker rm -f my-nginx # 强制删除运行中的容器
(6)查看容器详情与资源占用
docker inspect my-nginx
docker stats my-nginx
docker stats 预期输出:
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
f3a1b2c4d5e6 my-nginx 0.00% 3.5MiB / 7.7GiB 0.05% 1.2kB / 0B 0B / 0B
3.3 容器的生命周期
created → running → paused → running → stopped → deleted
↑ ↓
└────── restart ──────┘
对应命令:
docker create # 创建但不启动
docker start # 启动
docker pause # 暂停
docker unpause # 恢复
docker stop # 停止
docker kill # 强制杀死
docker rm # 删除
3.4 容器为什么“轻量”
容器不是虚拟机,它不模拟硬件、不跑独立内核,而是直接复用宿主机内核,通过 Linux 的 namespace 做隔离、cgroups 做资源限制。所以:
- 启动速度:容器秒级,虚拟机分钟级
- 资源占用:容器 MB 级,虚拟机 GB 级
- 隔离性:容器进程级,虚拟机硬件级
详细原理见后文“知识扩展”部分。
四、仓库(Registry):镜像的“应用商店”
4.1 什么是仓库
仓库(Registry)是集中存放和分发镜像的服务。最常用的是 Docker Hub,此外还有阿里云容器镜像服务、Harbor、GitHub Container Registry 等。
这里要区分两个概念:
- Registry(注册中心):存放多个仓库的服务,如 Docker Hub。
- Repository(仓库):某个镜像的所有版本集合,如 nginx 仓库包含 1.25、1.24、latest 等标签。
关系如下:
Docker Hub (Registry)
├── library/nginx (Repository)
│ ├── 1.25 (Tag)
│ ├── 1.24 (Tag)
│ └── latest (Tag)
└── library/mysql (Repository)
├── 8.0 (Tag)
└── 5.7 (Tag)
4.2 仓库常用操作
(1)登录仓库:docker login
docker login
# 或登录私有仓库
docker login harbor.example.com
(2)给镜像打标签:docker tag
推送前必须将镜像命名为 仓库地址/命名空间/名称:标签 格式:
docker tag myapp:v1.0 harbor.example.com/dev/myapp:v1.0
(3)推送镜像:docker push
docker push harbor.example.com/dev/myapp:v1.0
预期输出:
The push refers to repository [harbor.example.com/dev/myapp]
5f70bf18a086: Pushed
…
v1.0: digest: sha256:xxxx size: 1234
(4)拉取镜像:docker pull
docker pull harbor.example.com/dev/myapp:v1.0
(5)搜索镜像:docker search
docker search nginx –limit 5
预期输出:
NAME DESCRIPTION STARS OFFICIAL
nginx Official build of Nginx. 19000 [OK]
…
4.3 搭建私有仓库
使用官方 registry 镜像,一行命令即可:
docker run -d -p 5000:5000 –name registry \\
-v /data/registry:/var/lib/registry \\
registry:2
推送镜像到本地仓库:
docker tag myapp:v1.0 localhost:5000/myapp:v1.0
docker push localhost:5000/myapp:v1.0
查询仓库中的镜像:
curl http://localhost:5000/v2/_catalog
# 输出:{"repositories":["myapp"]}
注意:Docker 默认要求 HTTPS,若使用 HTTP 的私有仓库,需要在 daemon.json 中配置 insecure-registries:
{
"insecure-registries": ["localhost:5000"]
}
修改后重启 Docker 服务。
4.4 配置镜像加速器
国内拉取 Docker Hub 镜像较慢,可配置加速器。编辑 /etc/docker/daemon.json:
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://mirror.ccs.tencentyun.com"
]
}
重启生效:
sudo systemctl daemon-reload
sudo systemctl restart docker
docker info | grep -A 5 "Registry Mirrors"
五、三者关系再梳理:一张表看懂
| 本质 | 只读模板 | 运行实例 | 存储分发服务 |
| 状态 | 静态 | 动态 | 静态存储 |
| 可读写 | 只读 | 可读写 | 只读(对使用者) |
| 生命周期 | 长期 | 临时/可长可短 | 长期 |
| 类比 | 类 / 菜谱 | 对象 / 菜肴 | GitHub / 应用商店 |
| 常用命令 | pull、build、rmi | run、exec、logs | login、push、search |
| 数量关系 | 1 个镜像 → N 个容器 | N 个容器 ← 1 个镜像 | 1 个仓库 → N 个镜像 |
一个完整的开发流程:
# 1. 编写 Dockerfile 构建镜像
docker build -t myapp:v1.0 .
# 2. 本地运行容器验证
docker run -d -p 8080:8080 –name myapp myapp:v1.0
# 3. 打标签并推送到仓库
docker tag myapp:v1.0 registry.example.com/dev/myapp:v1.0
docker push registry.example.com/dev/myapp:v1.0
# 4. 在服务器上拉取并运行
docker pull registry.example.com/dev/myapp:v1.0
docker run -d -p 80:8080 registry.example.com/dev/myapp:v1.0
六、知识扩展:容器隔离的底层原理
容器之所以能做到“看起来像一台独立机器”,靠的是 Linux 内核的两大机制:
6.1 Namespace:负责“看不见”
Namespace 让容器内的进程只能看到自己命名空间内的资源。Docker 主要使用了 6 种:
| PID | 进程 ID | 容器内 PID 从 1 开始,看不到宿主机进程 |
| NET | 网络 | 独立网卡、IP、端口 |
| MNT | 挂载点 | 独立文件系统视图 |
| UTS | 主机名/域名 | 独立 hostname |
| IPC | 进程间通信 | 独立信号量、消息队列 |
| USER | 用户/组 | 容器内 root 可映射为非 root |
6.2 Cgroups:负责“用多少”
Control Groups 限制容器能使用的资源:
docker run -d –cpus="1.5" –memory="512m" nginx:1.25
上面的命令限制容器最多用 1.5 个 CPU 核、512MB 内存。查看限制:
docker inspect -f '{{.HostConfig.Memory}}' <container_id>
# 输出:536870912(单位字节)
6.3 与虚拟机的本质区别
| 隔离级别 | 进程级(namespace) | 硬件级(Hypervisor) |
| 内核 | 共享宿主机内核 | 独立 Guest OS 内核 |
| 启动速度 | 秒级 | 分钟级 |
| 镜像大小 | MB 级 | GB 级 |
| 隔离强度 | 较弱 | 强 |
| 适用场景 | 微服务、CI/CD、快速迭代 | 强隔离、异构 OS |
一句话总结:虚拟机虚拟的是硬件,容器虚拟的是操作系统。
七、常见错误与最佳实践
7.1 常见错误
错误 1:容器退出后数据丢失
docker run -d mysql:8.0
docker rm -f <container>
# 数据库数据全没了!
原因:数据写在容器可写层,容器删除即丢失。
解决:使用数据卷。
docker run -d -v mysql-data:/var/lib/mysql mysql:8.0
错误 2:镜像越构建越大
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y build-essential
COPY . .
RUN make
# 最终镜像包含了编译工具链,几百 MB 浪费
解决:多阶段构建。
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o app .
FROM alpine:3.19
COPY –from=builder /src/app /app
CMD ["/app"]
错误 3:在容器里存密钥
ENV DB_PASSWORD=123456 # 会被 docker history 看到!
解决:使用 –env-file、Docker Secret 或外部密钥管理。
错误 4:使用 latest 标签上生产
解决:始终使用明确的语义化版本标签,如 v1.2.3。
7.2 最佳实践清单
- ✅ 一个容器只跑一个主进程
- ✅ 镜像尽量小(Alpine / Distroless)
- ✅ 使用非 root 用户运行
- ✅ 数据持久化用 volume,不用容器层
- ✅ 敏感信息用 secret,不写进镜像
- ✅ 标签使用语义化版本,禁用 latest 上生产
- ✅ 用 .dockerignore 排除无关文件
- ✅ 定期清理无用镜像和容器
清理命令:
docker system prune -a –volumes
⚠️ 该命令会删除所有未使用的镜像、容器、网络和卷,生产环境慎用。
八、总结与练习
8.1 核心总结
8.2 练习题
练习 1:拉取 redis:7.2-alpine 镜像,后台启动一个名为 my-redis 的容器,映射宿主机 6379 端口,并用 redis-cli 连接验证。
提示:docker run -d –name my-redis -p 6379:6379 redis:7.2-alpine,然后 docker exec -it my-redis redis-cli ping,预期返回 PONG。
练习 2:查看 nginx:1.25 镜像的分层结构,并解释为什么 docker history 中有的层大小为 0。
提示:使用 docker history nginx:1.25,大小为 0 的层通常是元数据指令(如 ENV、CMD、EXPOSE),不产生实际文件变更。
练习 3:编写一个 Dockerfile,基于 python:3.12-slim 构建一个 Flask 应用镜像,要求:
- 使用非 root 用户运行
- 只复制必要文件
- 使用 CMD 启动
提示:使用 USER 指令切换用户,COPY requirements.txt 后再 COPY .,利用缓存。
练习 4:搭建一个本地私有 Registry,将练习 3 构建的镜像推送到该仓库,再从另一台机器(或删除本地镜像后)拉取验证。
提示:docker run -d -p 5000:5000 registry:2,注意配置 insecure-registries。
练习 5(思考题):为什么同一个镜像可以启动多个容器而互不影响?请从镜像分层和容器可写层的角度解释。
提示:镜像层只读且共享,每个容器在镜像之上拥有独立的可写层(Copy-on-Write),修改只发生在自己的可写层中。
如果本文对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续更新的最大动力!有任何问题也欢迎在评论区留言交流。

