尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Docker镜像与容器的核心区别:概念、原理与实战排查

Docker镜像与容器的核心区别:概念、原理与实战排查 刚开始接触 Docker 的人十个有九个会纠结同一个问题镜像 Images 和容器 Container到底是不是同一个东西为什么docker images看不到正在运行的服务docker ps又看不到下载好的镜像这两个词看起来像同义词实际上中间隔着一条“运行时”的分界线很多人没跨过去后面学数据卷、网络、Compose、K8s 的时候就会越学越拧巴。我用了很多年 Docker也帮不少同事排查过环境和概念上的问题。这篇文章不打算搬运官方文档而是按照我自己理解 Docker 的方式把镜像、容器以及它们之间“构建、运行、管理”这条链路讲清楚顺便把安装、启动、网络、镜像下载这些环节里最容易踩的坑一起放进来。适合刚入门 Docker 的同学也适合那些“明明命令背得很熟但出了问题就不知道从哪下手”的人。1. 先别急着敲命令把镜像和容器的关系弄明白1.1 我自己的理解框架模板与实例如果用一句话给镜像和容器定位我会这样说镜像是“只读的交付物”容器是“可写可跑的进程实例”。打个比方镜像很像一套房子设计图加精装样板间它包含了完整的文件系统、目录结构、系统配置和运行环境但它是“静态”的不会自己产生过程、联网、监听端口容器则是你拿着这套设计图实际装修好并住进去的那套房子它继承了样板间的一切但它有自己独立的生活痕迹——你放了东西、改了布局、产生垃圾这些改动都发生在容器自己的空间里不会回写到那张“图纸”本身。对应到命令上docker pull nginx是下载一套样板间图纸docker run nginx是用图纸造出一套能住人的房子docker ps看到的是正在住的房子docker images看到的永远是那一套套图纸。很多初学者以为“我 run 了镜像镜像体积应该变小”实际上镜像依然是原来那个镜像容器则是在镜像基础上叠加了一套可写层最终以进程的形式存在。1.2 镜像不是一张“静止的照片”分层机制才是关键镜像的官方定义是“只读模板”这个词容易让人误解以为镜像是把整个操作系统拍一张快照一次性变成一个大文件。实际不是这样。Docker 镜像是分层构建的。每一层都是一组文件系统的变更记录比如“新增了某个目录”“复制了某个二进制文件”“执行了 apt-get 安装了什么包”。当你下载一个常见的基础镜像它会由很多层叠加而成每一层都有独立的 ID 和哈希值。Docker 在拉取镜像的时候会按层去拉层与层之间如果已经存在就能复用不用重新下载。这也是为什么你第二次 pull 一个差不多环境镜像时速度明显变快。层与层之间靠联合文件系统UnionFS叠加起来对外呈现为一个完整的根文件系统。现代的 Docker 在 Linux 上默认使用 OverlayFS 实现数据卷、挂载、快照备份也都是围绕这层“叠加”机制展开的。这里有个很重要的推论既然镜像是只读的那么你直接修改容器里的文件并不会让镜像本身变大或变小。想让镜像包含这些修改必须把容器的可写层“固化”成新的镜像层这一步对应docker commit或者更推荐的方式——写 Dockerfile用docker build构建。1.3 用命令行反推概念images、ps、run 到底在操作什么弄懂概念最直接的方法是把常用命令和它背后的对象一一对应起来docker images查看本机已有的镜像列表列出的是仓库名、标签、镜像 ID、创建时间和体积。docker ps查看正在运行的容器列表没有加-a时看不到已退出的容器。docker run基于镜像创建一个新容器并启动它。docker start启动一个已存在、但处于停止状态的容器不再重新创建。docker commit把容器的当前文件系统状态固化成新的镜像。我自己带新人的时候会让他们先做一个小练习按顺序执行docker pull alpine、docker run -d --name test alpine sleep 3600、docker images、docker ps然后对比这四个命令的输出。正常情况下images列表里只有 alpine 一个镜像ps列表里出现一个名字叫 test 的容器。一旦能解释清楚“为什么镜像只有一个容器却是从同一个镜像复制出来的另一个实体”概念关基本就过了。2. 一条 docker run 命令背后发生了什么2.1 拆解一个最常用的启动命令我们以docker run -d -p 8080:80 --name web nginx为例拆开看每个部分做了什么docker run告诉 Docker 客户端我要创建并启动容器。-d后台运行容器不会阻塞当前终端。-p 8080:80端口映射宿主机 8080 端口转发到容器 80 端口。--name web给容器命名否则 Docker 会随机分配一个名字。nginx镜像名本地没有的话Docker 会自动尝试从镜像仓库拉取。执行这条命令后Docker 客户端会把请求发送给 Docker DaemonDaemon 检查本地是否存在 nginx 镜像如果不存在就先去公共仓库拉取接着分配一个可写层创建网络命名空间设置端口转发规则最后通过容器运行时runc 或其他 OCI runtime在隔离环境中启动进程。很多人会遇到error response from daemon: failed to create task for container: ...这类报错。它通常不是镜像的问题而是容器运行时层面无法创建任务常见诱因包括Cgroups 驱动不一致、内核配置问题、资源限制冲突或者 Docker 升级后缓存目录异常。遇到这种错误先不要反复删容器重跑应该先把错误信息完整贴出来分析再考虑重置 Docker 的状态。2.2 从镜像层到容器文件系统的变化容器启动前Docker 会做这几件事把镜像的全部只读层挂载好。在最上面创建一个可写层也就是容器层。把宿主机上的某些路径“绑定”进容器这就是数据卷或 bind mount。设置容器的网络栈、主机名、DNS 等环境信息。执行镜像里配置的启动命令。容器层承担所有写入操作。你在容器里创建文件、删除目录、安装软件写入都发生在这一层。这种机制叫写时复制Copy-on-WritecoW读取某个文件时如果没有被修改过就直接读镜像层一旦要修改才先把文件从只读层复制到可写层再改动。这带来一个很实用的优势多个容器可以从同一个镜像启动互不影响。镜像层被所有容器共享可写层各自独立。同一个 nginx 镜像你可以同时跑三个容器每个容器改自己的配置文件谁也不干扰谁。也正因如此隔离性不是靠“每个容器都完整复制一份镜像”实现的而是靠“共享只读层 独立可写层 独立进程命名空间”实现的。这也是 Docker 镜像比虚拟机镜像轻量很多的根本原因。2.3 隔离的本质命名空间与 Cgroups镜像决定了容器里能看到什么文件容器运行时则决定了容器里能感知到什么系统资源。这里涉及 Linux 的两套机制命名空间Namespace和 Cgroups。命名空间负责隔离“看”的维度进程命名空间、网络命名空间、挂载命名空间、用户命名空间、UTS 命名空间等。容器里的进程看到的进程列表、网络接口、文件系统挂载点都只是它自己命名空间内的视图。你在容器里执行ps看不到宿主机上其他无关进程就是因为 PID 命名空间做了隔离。Cgroups 负责限制“用”的维度CPU 配额、内存上限、磁盘 IO 权重、进程数限制等。你可以在docker run时用--memory和--cpus限制容器资源。如果没有这些限制一个出问题的容器可能吃满宿主机全部内存反过来拖垮其他容器和宿主进程。理解了这两套机制你再看到 Windows 上 Docker Desktop 依赖 WSL2 或 Hyper-V就不会觉得奇怪。Docker Desktop 需要在 Windows 上创建一个轻量虚拟机来提供 Linux 内核因为 Windows 原生不支持 Docker 所需的这些内核能力。3. 实操从拉镜像到跑 MySQL再构建自己的镜像3.1 安装之前先确认虚拟化在 Linux 服务器上安装 Docker 相对顺畅执行官方脚本或者包管理器安装即可。真正容易出问题的是 Windows 和 Mac 上的 Docker Desktop。Windows 上最常见的一个启动报错是virtualization support not detected Docker Desktop failed to start because virtualization support is not enabled意思很明确虚拟化功能没开启。Docker Desktop 在 Windows 上要靠 WSL2 或 Hyper-V这两个都需要 CPU 支持虚拟化并且要在 BIOS/UEFI 里打开 VT-x 或 AMD-V还要在 Windows 功能里启用“虚拟机平台”和“适用于 Linux 的 Windows 子系统”。排查顺序建议这样打开任务管理器看性能页里的“虚拟化”是否显示“已启用”。没启用的话重启进 BIOS找到 Intel Virtualization Technology 或 SVM Mode开启后保存退出。Windows 功能里勾选“虚拟机平台”“Windows 虚拟机监控程序平台”“适用于 Linux 的 Windows 子系统”重启。执行wsl --status看 WSL 内核是否正常不正常就执行wsl --update。最后再启动 Docker Desktop。如果你用的是 Windows 家庭版Hyper-V 不一定可用优先走 WSL2 路线。很多人一直卡在启动阶段其实不是 Docker 软件的问题而是 Windows 组件没装全。3.2 拉镜像太慢的通用解法新装好 Docker第一次docker pull容易卡在“拉取镜像”的步骤进度条一直不动。这通常不是网络完全不通而是访问公共镜像仓库的链路过长或受限。通用的做法是配置镜像加速器也叫 registry mirror。在 Docker Desktop 的 Settings 里找到 Docker Engine 配置或者在 Linux 上编辑/etc/docker/daemon.json添加类似这样的内容{ registry-mirrors: [ https://your-mirror-address.example.com ] }配置完成后执行systemctl restart docker或重启 Docker Desktop。国内云厂商一般都有容器镜像加速服务自己去对应控制台申请一个专属加速地址填进去即可也可以参考公共文档里常见的加速地址。这里提醒一点不要为了提速去用来路不明的第三方镜像源。公共镜像源的完整性和安全性很难保证万一被投毒你的生产环境就危险了。优先用云厂商自带的加速服务私有化部署可以自建 registry 镜像缓存。另外如果你经常拉同一个镜像可以留意一下层复用。新版镜像和老版本共享基础层时只要基础层已经在本地就只需要拉取差异层速度会快很多。这也是为什么建议基础镜像尽量统一比如团队内统一用某个固定版本的 Ubuntu 或 Alpine既能省磁盘又能加速部署。3.3 跑一个带数据卷的 MySQL 容器只说概念容易飘实际跑一个 MySQL 容器很多问题就具体了。docker run -d \ --name mysql-test \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDyourpassword \ -v /data/mysql:/var/lib/mysql \ mysql:8.0这条命令里-e MYSQL_ROOT_PASSWORD是传入环境变量MySQL 镜像初始化时会用它创建 root 用户-v /data/mysql:/var/lib/mysql是挂载数据卷把容器里的 MySQL 数据目录映射到宿主机。为什么必须加这个数据卷因为镜像里的文件是只读的可写层虽然能写但容器一旦被删除可写层连同所有数据就都消失了。MySQL 的数据如果存在容器层里docker rm mysql-test之后数据全部丢失。挂载到宿主机目录后删容器、重建容器数据依然在。跑起来之后你可以用这三条命令做基础验证docker ps # 看容器是否在运行 docker logs mysql-test # 看 MySQL 初始化日志 docker exec -it mysql-test bash # 进入容器内部docker exec -it mysql-test bash进入容器后你会看到容器里有一套独立的文件系统但某些目录是从宿主机映射进来的。这时再回想第一节的内容——容器看到的/var/lib/mysql其实很大可能就在宿主机/data/mysql里这就是挂载命名空间的直观体现。实际部署中我还建议加上--restartalways让 Docker 在守护进程重启或容器异常退出时自动拉起容器。MySQL 这类有状态服务如果不加这个参数一旦宿主机重启容器不会自动启动服务就悄悄挂了。3.4 用 Dockerfile 构建第一个镜像拉别人的镜像只是第一步真正掌握镜像概念还得学会自己构建镜像。构建镜像的正规姿势是写 Dockerfile。FROM node:20-alpine WORKDIR /app COPY package.json ./ RUN npm install COPY . . EXPOSE 3000 CMD [node, server.js]这个 Dockerfile 的每一行最终都会变成一个或多个镜像层。FROM指定基础镜像WORKDIR设置工作目录COPY package.json ./把本机文件复制进镜像RUN npm install是在构建过程中执行命令这条命令产生的新文件会成为新的镜像层CMD指定容器启动时执行的命令。构建命令docker build -t my-app:v1 .-t my-app:v1是给镜像打的标签最后的.表示构建上下文目录。注意这个“构建上下文”它决定了 Docker 能把你本机哪些文件 COPY 进镜像。如果你在/root/project下构建但 Dockerfile 里写了COPY /etc/hosts ./它拷贝的其实不是宿主机系统里的/etc/hosts而是构建上下文里的同名相对路径这一点新手经常搞错。构建完成后你会在docker images里看到自己的镜像。这时如果用docker history my-app:v1查看能看到每一层对应的 Dockerfile 指令和每一层产生的体积。强烈建议执行一次这个命令看过之后对“分层”的理解会瞬间加深。3.5 关于 docker commit我的真实看法docker commit可以把一个容器的当前状态固化成镜像。听起来很省事比如你进入容器装了一堆东西然后 commit 一下就能得到一个新镜像。但我很少推荐把 commit 当常规构建方式用。原因有几点commit 出来的镜像会包含容器里的临时文件、缓存、历史命令产生的残留内容体积往往偏大。commit 没有可复现性。你无法从 commit 出的镜像看出它是基于哪些步骤构建出来的几个月后想升级只能重新手工操作。commit 不会记录你在容器里用docker exec进入后执行的交互式命令镜像的内容和创建过程完全对不上。Dockerfile 则相反它把构建过程代码化、文本化可以放进 Git 管理。任何人拿到 Dockerfile 都能构建出基本一致的镜像而且能审计每一层做了什么。commit 只在少数场景下有用比如排查问题时临时固化容器的现场状态方便分发调试环境。日常构建镜像老老实实写 Dockerfile 才是正路。4. 镜像管理、瘦身与团队协作4.1 镜像命名、打标签与离线搬运镜像的完整名字一般长这样registry.example.com/team/project:1.2.3前半部分是仓库地址中间是镜像名冒号后面是标签。nginx:latest里的 nginx 实际上是docker.io/library/nginx的简写latest是默认标签。docker tag用于给镜像重新打标签它不复制文件只是给镜像 ID 多添一个名字。镜像 ID 才是真正的唯一标识标签更像一个指针。离线搬运镜像是很多内网环境中的刚需。常用做法docker save -o my-app.tar my-app:v1 docker load -i my-app.tarsave会把镜像的所有层打包成一个 tar 文件对方拿到后load就能导入。注意它和docker export的区别save保存的是镜像export导出的是容器当前的文件系统两者结构不同不能混用。save出的 tar 可以恢复成镜像并保留层级和标签export出的 tar 只是文件系统快照导入后没有镜像分层信息也无法完全还原原镜像的配置。4.2 基础镜像选型决定镜像体积下限镜像体积是所有入门者最先注意到的问题。一个 Ubuntu 基础镜像动辄几百兆一个 Alpine 基础镜像可能只有几兆差距就在基础镜像的选择上。ubuntu、debian体积大包管理系统成熟兼容性好。slim系列镜像通常是官方镜像的精简版去掉了文档、编译工具等运行不需要的组件。alpine基于 musl libc 和 BusyBox体积极小但部分依赖原生编译包的软件可能会有兼容性问题。distroless只包含运行时和应用程序不包含包管理器、shell安全性好但排障不便。选基础镜像没有绝对标准我的习惯是本机调试用完整版产出正式镜像时尽量用 slim 或者项目维护者官方提供的最小变体。比如 Java 项目优先用eclipse-temurin的 JRE 版本镜像Node 项目选node:20-alpine跑 Python 则是python:3.12-slim。装上镜像后可以用docker image inspect看每一层的体积找找“哪一层变大”往往能发现构建脚本里的缓存文件、包管理器缓存、日志文件这些垃圾源。4.3 多阶段构建与悬空镜像清理多阶段构建是压缩镜像体积最有效的技巧之一。核心思路是用一个大而全的镜像完成编译和依赖安装最后只把成品复制到一个精简运行镜像里。一个典型的 Go 项目FROM golang:1.22 AS builder WORKDIR /src COPY . . RUN CGO_ENABLED0 go build -o /app/server FROM alpine:3.20 COPY --frombuilder /app/server /app/server EXPOSE 8080 CMD [/app/server]第一个阶段里有完整的 Go 工具链第二个阶段只保留编译好的二进制和精简运行时。最终镜像体积可能从原来的 800MB 砍到 20MB 左右。这就是“构建环境”和“运行环境”分离的思路。镜像清理是另一个容易被忽略的地方。Docker 用久了docker images里会积累很多none标签的悬空镜像。它们通常是构建过程中被新版本替换掉的旧镜像层残留占磁盘但不显示具体用途。定期执行docker system prune它会清理停止的容器、未被使用的网络、悬空镜像和构建缓存。加上-a能清理所有未被容器引用的镜像但使用时建议谨慎确认没有保留价值再清理。我的习惯是每周跑一次docker system df看磁盘占用分布再决定要不要清理。4.4 自建镜像仓库与团队协作团队协作离不开镜像仓库。公共仓库虽然方便但企业内网环境通常需要自建。最简单的方式是直接跑一个 registry 容器docker run -d -p 5000:5000 --name registry registry:2这样你的机器上就有一个简易的私有镜像仓库。推送镜像docker tag my-app:v1 localhost:5000/my-app:v1 docker push localhost:5000/my-app:v1其他机器拉取时需要把localhost换成该服务器的 IP并且所有 Docker 客户端要在/etc/docker/daemon.json里把该地址配入insecure-registries否则 HTTPS 校验不通过。生产环境建议用 Harbor 这类完整的企业级镜像仓库它提供权限控制、镜像扫描、复制同步等功能。镜像仓库不是简单的文件存储它承担的是整个交付管线的“制品库”角色权限和版本策略值得认真规划。5. 高频问题的排查速查表5.1 Docker Desktop 起不来的几个原因前面提到过virtualization support not detected这是 Windows 上最常见的启动障碍。除此之外还有几类常见问题“Docker Desktop requires a newer WSL kernel version”WSL 内核过旧执行wsl --update更新。启动后一直停留在 “Docker Engine starting”多半是 Docker Engine 服务启动失败去排查引擎日志或尝试docker desktop stop后清理%LOCALAPPDATA%\Docker下的缓存再启动。报错里出现container runtime is not running注意看后面具体输出常见原因是 Docker 守护进程没有正常启动或者 WSL 发行版里的 Docker 服务内部出错。此时先看 Windows 服务里 Docker Desktop Service 的状态再看 WSL 里service docker status一层层定位。这一类问题大多不是“Docker 坏了”而是 Windows 组件、WSL 发行版、Docker 引擎之间的状态没同步。我建议先把 Docker Desktop 彻底退出再用管理员身份执行wsl --shutdown清掉所有 WSL 虚拟机然后重新打开 Docker Desktop很多玄学问题这样能解决。5.2 连接不到 Docker API 怎么办Windows 上还有一个经典报错error during connect: failed to connect to the docker api at npipe:////./pipe/docker-desktop-linux-...意思是 Docker 客户端连不上 Docker 引擎。核心原因一般是 Docker Desktop 根本没起来或者刚启动但引擎还没就绪。快速排查三步确认 Docker Desktop 图标状态是鲸鱼还是错误标识。在终端执行docker version分别看 Client 和 Server 的输出。Server 部分报错说明引擎没起来。打开 Docker Desktop 的 Troubleshoot 页面有 Restart、Clean / Purge data 等选项先从 Restart 开始。提醒一句不要看到这个报错就重装 Docker Desktop。重装后配置丢失不说问题的根源往往还在 WSL2 或虚拟化层。按顺序排查大概率只是引擎没启动。5.3 容器网络不通的排查顺序容器网络是另一个高频问题区。现象通常是宿主机能访问容器容器访问宿主机不通或者容器与容器之间不通。我梳理过一个通用的排查顺序先确认端口映射docker port 容器名看端口绑定是否正常。确认容器进程是否活着docker ps -a看状态是否 Exited。进入容器看网络栈docker exec -it 容器名 ping 目标地址不通时继续往下。查看容器网络模式默认是 bridge宿主机里执行docker network inspect bridge能看到网段。在宿主机上检查 iptables 规则是否异常。Docker 靠 iptables 做 DNAT 转发如果宿主机上有防火墙管理工具把 Docker 的链规则清了端口映射就会失效。检查宿主机内核的ip_forward是否开启。执行sysctl net.ipv4.ip_forward如果为 0容器对外通信基本不通临时开启用sysctl -w net.ipv4.ip_forward1。还有一种容易忽略的情况容器内 DNS 解析失败。docker run默认使用 Docker 内置 DNS 127.0.0.11如果宿主机 DNS 有问题容器解析域名也会失败。此时可以在容器里直接nslookup某个域名如果 ping IP 通但 ping 域名不通基本就是 DNS 配置问题考虑在 daemon.json 里调整dns配置。5.4 容器启动就退出、一直重启怎么定位第一次写 Dockerfile 的人经常遇到容器跑起来瞬间就退出。最常见原因镜像里没有前台进程。容器主进程一退出容器就停了。比如你构建了一个只复制文件、没有CMD的镜像docker run后什么都不做就退出。启动命令执行失败。脚本里路径写错、依赖缺失、端口被占用导致进程启动即崩。restartalways导致容器无限重启看起来像在“反复启动”。定位手段主要是日志。docker logs 容器名是第一步。如果容器已经退出用docker logs --tail 50看最后几十行。举例来说启动命令是python app.py但镜像里根本没有app.py日志会直接报 No such file or directory。这时把CMD改成绝对路径或者先进入镜像检查文件是否真的存在。防止“无限重启”的最好办法是调试阶段不要加--restartalways先裸跑起来确认稳定再加重启策略。5.5 权限和挂载相关的坑权限问题是数据卷最常见的坑。典型场景是容器以 root 用户运行向挂载目录写入文件但宿主机目录属主并不希望被 root 写入的文件污染权限。解决办法有几种在docker run时指定用户比如-u $(id -u):$(id -g)。在 Dockerfile 里用USER指令切换运行用户。对文件属主敏感的服务用专门的数据卷初始化脚本把权限调整好再启动。另外Linux 上 SELinux 开启时挂载目录可能被拒绝访问报错类型是 permission denied需要在脚本里加:z或:Z后缀例如-v /data:/data:Z。Mac 上偶尔也会遇到文件共享权限的问题需要在 Docker Desktop 的 Settings 里把对应目录加入 File Sharing 列表。6. 写给新手的几条实操习惯6.1 我日常绕不开的几个习惯用 Docker 这几年我慢慢养成一些固定习惯不一定是标准答案但对减少事故挺有帮助。命名永远显式指定。不管docker run还是docker network create我都用--name或-n把名字写清楚。一堆随机生成的容器名会让你不想去看哪个是哪个日志和资源占用排查会变得很痛苦。标签版本化。我几乎不用latest标签部署服务。latest的含义随时可能变今天跑的是这个版本明天拉一次就变成另一个版本没法复现部署现场。项目镜像全部打上语义化版本号或者直接用 Git 提交哈希作为标签方便回滚和追溯。一切配置进文本。用docker run敲完一遍参数后我会立刻整理成 docker-compose.yml哪怕只有一个服务。这样重建环境、换台机器部署只需复制一份文件不用靠记忆和聊天记录。6.2 少走弯路的几点心得镜像分层是 Docker 的精髓但很多教学内容把它讲得太抽象。你只要记住一个结论镜像是只读的基础设施容器是它跑起来后的现场数据要想持久化必须放到镜像外配置要想可维护必须写进代码里。如果这篇文章只能留下一个印象我希望是遇到容器问题先分清楚是“镜像构建阶段”还是“容器运行阶段”。构建阶段的报错发生在docker build里运行阶段的报错发生在docker run启动后。两个阶段的排查方向完全不同前者重点看 Dockerfile 和构建日志后者重点看docker logs和容器状态。这个边界一旦清晰你会发现 Docker 的绝大多数问题都有迹可循。我最初学 Docker 时也走了不少弯路反复刷教程却很少去思考“为什么镜像要从上到下分这么多层”。后来在性能和部署的实战中被磨过几次才真正把这些概念串起来。希望你读完之后能直接把这些经验用到自己的项目里少踩几个我用踩踩出来的坑。
返回列表