Docker 入门不需要背命令,先建立三个判断就够了:容器不是轻量虚拟机,而是被内核隔离的普通进程;镜像由只读层叠加而成,容器只是在最上面加了一层可写层;数据卷和网络决定了容器能不能持久化数据、能不能被别人访问。搞清这三点,剩下的命令都是查表的事。
容器与虚拟机的本质差别
虚拟机在物理机上跑一个 Hypervisor,再在 Hypervisor 上启动完整的 Guest OS,每个虚拟机自带一整套内核、驱动和 init 进程。容器不走这条路:容器里的进程直接跑在宿主机内核上,Docker 只是通过 Linux 内核提供的两组机制把它"看起来像独立机器"。
- namespace(命名空间)负责隔离:PID namespace 让容器看不到外部进程,NET namespace 给容器独立网卡与端口空间,MNT namespace 隔离挂载点,UTS 隔离主机名,IPC 隔离信号量与共享内存,USER 隔离 UID 映射。
- cgroup(控制组)负责限额:限制 CPU 权重与上限、内存上限、块设备 IO 带宽,并统计用量。这也是
docker run --cpus、--memory参数真正落到的地方。
因为共享内核,容器启动时间在百毫秒级,单机可以跑几十上百个容器;代价是隔离强度弱于虚拟机——一个内核漏洞可能穿透所有容器,也无法在容器里跑与宿主机不同的内核版本或 Windows 容器(除非开启虚拟化层)。生产上常见的做法是"容器跑应用、虚拟机做边界",或者用 Kata、gVisor 这类安全容器补上隔离短板。
一句话记法:虚拟机虚拟的是硬件,容器隔离的是进程。前者靠 Hypervisor,后者靠 namespace + cgroup。
镜像分层与 Dockerfile 最佳实践
镜像由若干只读层(layer)叠加而成,每一条会改动文件系统的 Dockerfile 指令产生一层。容器启动时,Docker 在所有只读层之上挂载一个可写层(copy-on-write),容器内的写操作先写可写层,删除容器时可写层一并丢弃——这就是"容器里的数据会丢"的根因。
分层还带来两个直接影响:一是层可以被多个镜像共享,本地磁盘占用远小于各镜像大小之和;二是构建时未变化的层直接命中缓存,所以 Dockerfile 里指令的顺序直接决定构建速度。
| 实践 | 原因 |
|---|---|
把 COPY package.json 与 npm install 放在 COPY . . 之前 |
源码改动不会让依赖层缓存失效,改一次代码不必重装依赖 |
| 用多阶段构建(multi-stage),构建工具只留在 builder 阶段 | 最终镜像不携带编译链,体积可从数百 MB 降到几十 MB |
合并 RUN,并在同一层里清掉 apt/yum 缓存 |
跨层删除文件不会减小镜像,删除动作本身也占一层 |
基础镜像固定到具体 tag 或 digest,不用 latest |
保证可重现构建,避免"昨天能跑今天不能跑" |
使用非 root 用户(USER)并写 .dockerignore |
缩小攻击面;避免把 .git、node_modules、密钥打进构建上下文 |
不把密钥写进 ENV 或 ARG |
层里可以还原出历史值,镜像推到仓库即泄露 |
容器生命周期
容器的状态是线性的:created → running → exited,中间可暂停(paused)。要理解一个细节:容器的主进程(PID 1)退出,容器就退出。所以 docker run -d nginx 能常驻,而 docker run -d ubuntu 立刻退出——不是因为镜像有问题,而是没有前台进程可跑。
因此容器里的应用必须在前台运行:nginx -g "daemon off;"、java -jar app.jar、node server.js。如果你在脚本里 nohup 或 systemctl start,容器会认为任务结束而退出。另外 PID 1 还负责回收僵尸进程,业务进程不处理 SIGTERM 会导致 docker stop 等满 10 秒后被 SIGKILL 强杀,产生脏数据和长尾延迟——应用要正确注册优雅退出逻辑。
数据卷与持久化
容器可写层随容器删除而消失,持久化有三条路,适用场景完全不同。
- 卷(volume):由 Docker 管理,落在
/var/lib/docker/volumes,跨容器共享、可备份、可迁移,是生产首选。docker volume create后挂到/data即可。 - 绑定挂载(bind mount):把宿主机任意目录挂进容器,开发时改代码即时生效最方便,但强依赖宿主机路径结构,跨主机不可移植,权限与 SELinux 标签也容易踩坑。
- tmpfs:只存在于内存,容器停止即消失,适合缓存和临时密钥,绝不适合业务数据。
数据库、上传目录、日志这些有状态数据必须落在卷或外部托管存储上。经验上还有一个容易忽略的点:卷里的数据不随镜像更新而迁移,升级镜像前要先确认数据目录路径和数据格式兼容,并做好备份——这一步的失误率远高于应用本身。
网络:bridge、host 与 overlay
默认情况下容器使用 bridge 网络:Docker 在宿主机建一个 docker0 网桥,给每个容器分配 veth 网卡与私有 IP,容器访问外网靠 iptables 做 SNAT,外部访问容器靠 -p 端口映射做 DNAT。
| 网络模式 | 行为 | 典型用途 |
|---|---|---|
| bridge(默认) | 容器有独立 IP,需 -p 映射端口;自定义 bridge 支持按容器名做 DNS 解析 |
绝大多数单机应用,同网络内容器可直接用服务名互访 |
| host | 容器与宿主机共享网络栈,无端口映射,直接用宿主端口 | 对网络性能敏感、或需要处理大量端口的场景 |
| none | 只有 loopback,无外网 | 离线批处理、安全隔离的构建任务 |
| overlay | 跨多台宿主机建立虚拟网络(依赖 Swarm 或外部 KV 存储) | 旧式跨主机容器通信;现代多主机编排建议直接用 Kubernetes 的 CNI |
| macvlan | 容器直接拿到物理网段的 MAC 与 IP | 需要容器在二层网络中被当作独立主机的遗留系统 |
默认 bridge 有个常见陷阱:容器之间只能靠 IP 互通,容器重启 IP 会变。正确做法是自建网络 docker network create app-net,把服务都接进去,之后用容器名当主机名访问——这也是 Compose 里服务名能当域名用的原理。
常用命令速查
| 目的 | 命令 |
|---|---|
| 构建镜像 | docker build -t app:1.0 . |
| 查看镜像层历史 | docker history app:1.0 |
| 后台运行并映射端口 | docker run -d --name app -p 8080:8080 -v appdata:/data app:1.0 |
| 进入容器排查 | docker exec -it app sh |
| 查看资源占用 | docker stats |
| 看日志(带时间戳、持续跟踪) | docker logs -f --tail 100 -t app |
| 查看容器详情(含退出码、挂载、网络) | docker inspect app |
| 清理无用资源 | docker system prune -a --volumes(慎用,会删未使用镜像与卷) |
单机跑多容器时不必手写一长串 docker run,用 Compose 把服务、网络、卷和依赖关系写进一个 YAML,一条 docker compose up -d 即可复现环境。再往上就是容器编排,见 Kubernetes 核心概念;如果还在纠结容器和虚拟机的选型,可以先看 什么是云服务器。
常见问题
Docker 容器和虚拟机到底哪个性能更好?
容器没有硬件虚拟化开销,CPU 与内存的损耗通常在个位数百分比,启动是毫秒级;虚拟机的开销来自 Guest OS 与 Hypervisor 层,启动以分钟计。但容器共享内核,隔离强度弱,两者不是替代关系而是边界不同:容器适合打包应用、快速扩缩,虚拟机适合做租户隔离与内核级安全边界。
容器重启后数据为什么没了,怎么持久化?
因为容器内的写操作落在最顶层的可写层,容器删除后可写层随之销毁。需要持久化的目录要用 -v 卷名:/path 或绑定挂载宿主机目录,把数据放到容器之外。注意 docker restart 不会丢数据,只有 docker rm 删除容器时才会丢失可写层。
docker run 之后容器立刻退出了是怎么回事?
容器的主进程退出,容器就退出。常见原因是启动命令是个一次性任务(如 ubuntu 默认跑 bash 且没有 TTY),或者用 nohup、systemctl 把应用放到后台导致前台无进程。用 docker logs 容器名 看退出原因,把应用改成前台运行即可常驻。
同一个 bridge 网络里的容器为什么互相 ping 不通?
默认 docker0 网桥不支持容器名解析,且容器重启后 IP 会变化,用 IP 互访很容易断。建议先 docker network create 创建自定义 bridge 网络,把所有相关容器接入同一网络,然后用容器名或 Compose 服务名作为主机名访问,DNS 由 Docker 内置解析器负责。
延伸阅读
- Kubernetes 核心概念:Pod、Deployment、Service
- 云原生架构入门:微服务、容器与 CI/CD 怎么落地
- 云服务器选购指南:配置、带宽与计费方式怎么选
- 服务器监控告警怎么搭:从指标采集到告警收敛:容器化之后监控口径会变,这篇讲清指标该怎么采。
- IaaS、PaaS、SaaS 到底怎么区分?一张表看懂云计算服务模型:容器处在 IaaS 与 PaaS 之间,这篇帮你定位它的边界。
- Nginx 性能调优清单:worker、连接数与缓存怎么设:容器里跑 Nginx 时,连接数与文件描述符要一起调。