Docker核心知识体系:镜像、容器、数据卷、网络与编排全解析
搞过几年容器化的开发者多多少少都有过这种体验docker run能跑起来一个 Nginx但换个场景就抓瞎了——镜像为什么越构建越大容器删了数据就没了两个容器怎么互相访问这些问题不看文档还真说不明白。Docker 核心知识体系说白了就是把这套东西的前因后果捋清楚镜像、容器、仓库、数据卷、网络、编排每块都有它的设计逻辑。本文按我自己的理解把这些内容串成一个完整骨架跟着过一遍至少能让你遇到问题时不慌知道该往哪个方向排查。适合刚学 Docker 的开发者也适合用了很久但一直靠“复制粘贴命令”撑场面的同学。1. 核心架构认知镜像、容器、仓库到底在解决什么问题先把 Docker 最底层的三个概念讲透因为后面所有的操作都是围绕它们展开的。镜像Image、容器Container、仓库Repository这三者的关系用“类、实例、代码托管平台”来类比就很好懂镜像是模板容器是模板跑起来的实例仓库是存放和分发模板的地方。1.1 镜像的分层结构是理解 Docker 的钥匙镜像是只读的、分层存储的文件集合。每一条构建指令比如RUN、COPY都会生成一个新的只读层层与层之间是堆叠关系。这种设计最大的好处是多个镜像可以共享底层数据拉取新镜像时只需要下载新增的层。比如你本地有了一个 Ubuntu 镜像再拉一个基于 Ubuntu 构建的 Python 镜像时底层那几层已经存在直接复用省流量也省磁盘。这跟 Git 的提交记录很像每一层相当于一次 commit构建历史就是一条提交链。联合文件系统Union FS负责把这些层“合并”成一个统一视角。容器运行时会在镜像层之上再加一个可写层。你在容器里创建文件、修改配置所有变更都发生在这一层镜像本身不会被改动。这就是“写时复制”的核心表现容器读文件时数据来自最底层写文件时Docker 会先把文件复制到顶层可写层再在顶层做修改底层保持不变。这个机制带来的实际意义是多个容器共用同一个镜像不会互相污染。容器删除后可写层也跟着删除所以容器是“无状态”的。想让数据活过容器生命周期必须用数据卷后面会细讲。1.2 容器的本质一个被隔离的进程容器的隔离能力来自 Linux 内核的 Namespace 和 Cgroups。Namespace 让容器拥有独立的进程树、网络栈、文件系统挂载点、用户空间等视图Cgroups 则限制容器能使用的 CPU、内存、IO 等资源配额。很多人把容器和虚拟机混为一谈这是认知上的一个大坑。虚拟机里面跑的是一个完整操作系统有独立的内核容器则是宿主机上的普通进程只是被“包装”得看起来像一个独立环境。这个差异决定了容器启动快秒级、资源占用小但隔离性不如虚拟机强。实操中你要知道几个关键命令# 查看容器的进程信息 docker ps # 进入容器内部查看进程列表 docker exec -it 容器ID ps aux # 查看容器资源占用 docker stats容器内 PID 1 进程通常就是我们指定的启动命令或入口进程。它负责接收信号、管理子进程。如果 PID 1 进程退出容器就进入退出状态其他子进程也会被杀掉。这就是为什么很多镜像会用一个初始化系统如 tini来作 PID 1避免僵尸进程问题。1.3 仓库镜像分发的基础设施仓库Registry解决的是“镜像怎么共享”的问题。官方有 Docker Hub企业内部常用 Harbor、Nexus 或阿里云容器镜像服务。镜像的完整名称通常是仓库地址/命名空间/镜像名:标签比如nginx:1.25省略了仓库地址默认走 Docker Hub。镜像的标签Tag不是必须唯一的同一个镜像可以打多个标签。但要注意标签是可变的你拉到的latest可能跟上个月看到的完全不是一个东西。生产环境建议固定到具体版本甚至使用镜像摘要Digest来锁定内容docker pull nginx:1.25sha256:xxxx这部分只要理解容器、镜像、仓库是“三位一体”的关系后面的命令、数据卷、网络、编排学起来就不会觉得东一榔头西一棒槌。2. 镜像与容器实操从拉取到运行的核心命令闭环理解了架构再看命令就顺理成章了。实际工作中最常用的不是几十个命令车轮战而是几个固定组合。我按一个镜像从获取到销毁的完整生命周期来串。2.1 镜像的获取、查看与清理拉镜像用docker pull查看本地镜像用docker images。两个命令背后都有值得说的细节。docker images显示的每一行核心信息是 REPOSITORY、TAG、IMAGE ID、SIZE。IMAGE ID 是镜像唯一的 SHA256 标识实际使用中只需要写前 12 位甚至前 6 位就能定位。SIZE 显示的是镜像解压后的总大小不是下载体积更不是运行时占用——因为你可能跟其他镜像共享底层数据。清理镜像时很容易踩坑正在被容器使用的镜像不能直接删必须先删容器或者确认容器不在运行状态。常用清理组合# 删除所有悬空镜像没有任何标签引用的 docker image prune # 删除所有未被容器使用的镜像 docker image prune -a # 强制删除慎用 docker rmi -f 镜像ID-f强制删除是把镜像从本地强制移除但这往往掩盖了依赖关系问题不到万不得已不建议用。2.2 容器生命周期的标准操作运行容器不是只靠docker run它其实是“创建 启动 挂载 配置网络”的集合。常用的docker run参数如下docker run -d \ --name web \ -p 8080:80 \ -v /opt/data:/app/data \ --restart unless-stopped \ nginx:1.25各参数含义-d后台运行容器返回容器 ID。--name给容器起名字不指定的话 Docker 会随机生成一个难记的名字。-p 主机端口:容器端口端口映射宿主机 8080 端口转发到容器 80 端口。-v 宿主机目录:容器目录挂载数据卷后面细讲。--restart unless-stopped容器退出后自动重启除非手动 stop。这是生产部署里比较稳妥的策略。容器运行起来后日常操作围绕这几点# 查看运行状态 docker ps -a # 查看容器日志 docker logs -f web # 进入容器内部分配一个交互式终端 docker exec -it web /bin/bash # 停止/启动/重启 docker stop web docker start web docker restart web # 停止所有容器批量操作常见需求 docker stop $(docker ps -q)docker logs有个细节-f是 follow 模式相当于tail -f。如果容器输出太混乱可以用--tail 50只看末尾 50 行也可以加--since 1h只看最近 1 小时日志。2.3 进入容器与文件拷贝的常见误区docker exec是进容器操作的主要手段。很多人把docker attach和docker exec搞混。attach是连接到容器的主进程终端当你 CtrlC 退出时通常会连带着终止容器主进程exec是在容器里启动一个新进程退出它不影响容器本身。所以日常调试习惯用exec别用attach。容器内安装调试工具是另一个常见需求。比如想用curl测接口但容器里没有这个命令。直接连上去运行apt-get install curl是能装上但容器一删除重新启动后改动全部丢失——因为可写层跟着容器销毁了。正确做法是把调试工具的安装写进 Dockerfile重建镜像。临时场景下可以用docker cp把宿主机文件拷进容器docker cp /tmp/debug.sh web:/opt/debug.sh临时调试可以目标环境别依赖这种方式。运行容器时还有一个高坑点前台运行与后台运行的混淆。docker run nginx不带-d你会发现终端卡住不动日志不断刷屏。这是因为 Nginx 本身是前台进程docker run默认将容器进程的 stdout 挂到当前终端。这也是 Docker 的一个设计原则容器里的主进程必须保持在前台运行否则容器会立即退出。所以像 Nginx、Redis 这类服务镜像内的启动脚本已经把 daemon 化开关关掉了。3. 数据持久化数据卷与绑定挂载的正确姿势容器是易失的可写层随容器消亡而消失于是“数据持久化”成为容器方案里绕不开的话题。Docker 提供了三种主要手段绑定挂载Bind Mount、数据卷Volume、以及临时文件系统tmpfs。3.1 数据卷与绑定挂载的取舍绑定挂载是把宿主机目录直接挂载到容器路径写法就是-v /宿主机路径:/容器路径。它直观、方便测试但跨主机迁移困难且依赖宿主机目录结构。数据卷则是 Docker 自己管理的存储区域默认位于/var/lib/docker/volumes/下可以跨容器共享备份和迁移也更方便。举一个具体的选择场景开发环境跑个 Nginx 静态站点源码在宿主机某目录希望改完代码容器立即生效用绑定挂载。跑数据库MySQL、Postgres数据必须安全且希望后续做备份用命名数据卷。先创建数据卷再挂载是可靠的做法docker volume create mysql_data docker run -d --name mysql \ -e MYSQL_ROOT_PASSWORDsecret \ -v mysql_data:/var/lib/mysql \ mysql:8-v mysql_data:/var/lib/mysql中mysql_data不带路径前缀Docker 会把它当数据卷处理如果写的是绝对路径就是绑定挂载。这个细节很多人会弄混淆。3.2 数据卷的备份与迁移数据卷备份其实很简单核心思路是用一个临时容器把数据卷打包成压缩文件docker run --rm \ -v mysql_data:/volume \ -v /opt/backup:/backup \ alpine tar czf /backup/mysql_data.tar.gz -C /volume .这里用alpine镜像当“搬运工”把mysql_data卷的内容打包到宿主机/opt/backup下。恢复数据也类似把 tar 包解到卷路径即可。这个临时容器用--rm标志跑完就自动删除不影响现有环境是很典型的 Docker 一次性任务模式。3.3 文件权限与权限陷阱挂载数据卷之后最容易遇到的问题就是“权限拒绝”。宿主机目录的 UID/GID 与容器内进程的 UID/GID 不一致时容器内的应用可能无法读写挂载目录。比如 MySQL 镜像默认用mysql用户UID 999宿主机挂载目录如果归 root 所有初始化时就可能报Permission denied。解决办法有两种在宿主机上chown -R 999:999 /path/to/mount使目录属主与容器内用户一致。在 Dockerfile 里通过USER指令把容器运行用户改成与宿主机目录属主一致。数据卷的权限问题远比想象中频繁。特别是把原有系统的目录迁进容器时一定要先确认宿主机的 UID/GID别看到Permission denied就盲目chmod 777那不是长久之计安全隐患也大。4. 网络模型容器之间、容器与宿主机之间怎么通信网络是 Docker 中最容易让人犯迷糊的部分。因为容器默认有自己的网络命名空间对外通信需要端口映射容器之间要通信还得借助 Docker 网络。我把这层逻辑拆成三块讲。4.1 默认网络模式bridge、host、noneDocker 安装后会默认创建三个网络bridge、host、none。bridge默认模式。所有容器通过 Docker 网桥通常是 docker0互联容器之间的流量走虚拟网络宿主机通过 NAT 让容器访问外网。容器对外提供服务需要做端口映射。host容器不隔离网络直接使用宿主机网络栈。启动快、性能好但端口会直接占用宿主机可能有冲突。none容器没有网络配置只回环接口可用。适合完全离线的安全任务。docker run加--network host或者--network none可以切换模式。生产环境大部分场景用 bridge因为隔离性好配合自定义网络还能实现服务发现。4.2 自定义网络与容器 DNS 解析用默认 bridge 网络容器之间要通过 IP 互相访问但容器重启后 IP 会变不可靠。解决办法是创建自定义 bridge 网络Docker 内置 DNS 会自动解析容器名docker network create my-net docker run -d --name app1 --network my-net nginx docker run -d --name app2 --network my-net nginx此时在 app1 容器内可以直接curl http://app2不用关心 app2 的 IP。这算是容器编排的最基础雏形也是后面 Docker Compose“服务名互访”埋下的伏笔。自定义网络还有一个隐含好处只有挂在同一网络的容器才能互相通信隔离性天然比默认 bridge 好。默认情况下所有不指定网络的容器都会连进默认 bridge彼此之间是可以互通的这在多项目并存时容易出现意外的访问路径。4.3 端口映射与宿主机访问对外提供服务时端口映射是必经之路。-p 8080:80的含义是宿主机 8080 端口进入的流量转给容器 80 端口。要注意-p默认只绑定在0.0.0.0上也就是所有网卡接口都会监听包括公网。这带来一个安全问题。如果你在云服务器上跑了个容器-p 6379:6379把 Redis 端口暴出去了而 Redis 又没有密码那就是裸奔。限制监听地址可以这样写-p 127.0.0.1:6379:6379只允许本地访问不让外部网络碰到。这是安全基线不是进阶技巧。生产环境里对数据库、Redis、管理后台这类敏感服务一定要指定监听地址或者用防火墙规则兜底。容器访问宿主机服务的场景也常见。比如容器里的应用要连宿主机上的 MySQL可以在容器内用host.docker.internalDocker Desktop 支持或者直接用宿主机在 docker0 网桥上的 IP 来访问。Linux 环境下一般查ip addr show docker0拿到网桥地址。这个属于网络里最阴间的部分踩过一次就记住了。5. Dockerfile 编写要点从能构建到构建得优Dockerfile 是镜像的构建脚本也是很多人从“会用 Docker”走向“会写 Docker”的分水岭。核心就是几点层数控制、缓存利用、多阶段构建、安全加固。5.1 指令层级与构建缓存的关系Dockerfile 的每一条指令对应一个层。指令越多镜像越大构建时间越长。更关键的是Docker 在构建时会逐条检查缓存如果当前指令没有变化就复用之前的缓存层一旦某条指令变了它之后的所有指令都会重新执行缓存全部失效。基于这个机制指令顺序很有讲究。把“不怎么改变”的依赖安装放前面把“经常变更”的代码复制放后面。常见典型顺序FROM node:18-alpine WORKDIR /app # 先复制依赖清单文件利用缓存 COPY package.json package-lock.json ./ RUN npm ci --prod # 最后复制应用代码 COPY . . CMD [npm, start]如果不把package.json单独复制而是直接COPY . .那么每次源码变更都会让RUN npm ci重新执行构建速度会明显变慢。这是初级和熟练写法之间的典型差异。5.2 多阶段构建瘦身镜像的标准套路多阶段构建的核心思想是用一个“构建环境”镜像把程序编译打包再用一个“运行环境”镜像只放最终产物。最终镜像不需要包含编译器、依赖源码、构建工具体积能小得惊人。以 Go 程序为例# 第一阶段编译 FROM golang:1.21 AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED0 GOOSlinux go build -o app # 第二阶段运行 FROM alpine:3.19 WORKDIR /app COPY --frombuilder /app/app . EXPOSE 8080 CMD [./app]最终运行镜像里只有二进制文件和一个极简的 Alpine 环境。同样的思路适用于 Java先 Maven 打包再传 JAR 到 JRE 基础镜像、前端先 Node 构建静态资源再传给 Nginx 镜像、Python先编译依赖再精简运行环境。多阶段构建的实际价值不仅体现在镜像仓库的存储成本上更体现在镜像拉取耗时、启动速度、攻击面减少上。一个几 GB 的后端镜像精简到几百 MB 甚至几十 MB日常维护体验完全不同。5.3 常用指令的细节坑CMD与ENTRYPOINT前者提供默认启动命令可被docker run命令覆盖后者不能被覆盖适合定义固定入口。两者组合使用时ENTRYPOINT里的参数会被docker run附加的参数接在后面。COPY与ADD官方推荐用COPYADD多了自动解压和远程 URL 下载能力但行为不稳定容易造成意外。没有特别需要就不要用ADD。EXPOSE只是声明容器内程序监听的端口它本身不产生端口映射。很多初学者以为写了EXPOSE就能从外部访问其实还需要docker run -p。USER默认容器以 root 运行安全风险大。用USER指令切换到非 root 用户是生产环境的基本要求。写 Dockerfile 时还要注意.dockerignore文件。它和.gitignore一个作用把不需要的文件排除在构建上下文之外。构建上下文是指 Docker CLI 发送给 Docker Daemon 的文件集合不写.dockerignore本地巨大的node_modules、target目录会被整个打包发送构建慢到怀疑人生。6. Docker Compose多容器编排的第一站单容器用docker run没问题但一旦涉及多个服务比如后端 数据库 缓存 消息队列手工敲命令就变成灾难。Docker Compose 把多个容器的启动逻辑写进一个 YAML 文件一键拉起、一键停止。它不算生产级编排工具那是 Kubernetes 干的事但确是日常开发和单机部署效率最高的方案。6.1 服务定义与依赖关系最小可用的docker-compose.yml长这样services: web: build: . ports: - 8080:80 depends_on: - db db: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: secret volumes: - db_data:/var/lib/mysql volumes: db_data:depends_on控制启动顺序但它只保证依赖的服务先启动不保证依赖服务“已经可用”。比如 Web 容器启动时 MySQL 容器已经在运行了但 MySQL 内部可能还没初始化完成。解决这个问题的通用手段是在应用代码里加连接重试机制或者在入口脚本里等待数据库就绪后再启动主进程。6.2 Compose 环境配置管理大多数项目有开发环境、测试环境、生产环境的配置差异。Compose 支持环境变量插值.env文件中定义的变量可以直接在docker-compose.yml里引用services: backend: image: backend:${TAG:-latest} environment: DB_HOST: ${DB_HOST:-db}${TAG:-latest}表示如果TAG变量未设置就使用latest作为默认值。这个语法在实际部署中很有用可以做到同一份 Compose 文件适配多种环境。Compose 常用命令# 启动并后台运行 docker compose up -d # 查看服务状态 docker compose ps # 查看所有服务日志 docker compose logs -f # 重建某个服务代码变更后常用 docker compose up -d --build web # 停止并删除容器、网络、数据卷-v 慎用 docker compose down -vdown -v会连数据卷一起删除数据会彻底消失这个参数务必谨慎。没有-v时数据卷保留下次up还能复用。6.3 网络与项目隔离Compose 会自动为每个项目创建一个独立网络服务名即主机名。web服务可以通过http://db:3306访问数据库。多项目并存时网络天然隔离互不干扰。这是 Compose 设计的精髓所在你不用关心容器 IP只需按服务名通信。如果需要让两个 Compose 项目共享网络可以显式指定外部网络services: app: networks: - shared-net networks: shared-net: external: true这个场景常见于一个项目跑基础设施如 Nginx 入口、监控另一个项目跑业务应用业务应用要挂进基础设施的网络里。没有external: trueCompose 默认会创建新网络两个项目就对不上了。7. 常见问题与排查技巧实录最后这一部分是我自己想重点分享的因为文档里很难找到这些“血泪经验”。按实际踩坑频率排个序。7.1 容器退出码的排查思路docker ps -a里退出码就是问题线索。退出码含义常见原因与排查0正常退出主进程主动结束通常是业务逻辑触发退出看日志定位1程序错误应用启动失败、配置错误、依赖缺失查容器日志137SIGKILL内存超限被 OOM Killdocker stats看占用容器内查dmesg139SIGSEGV段错误常见于底层函数库或架构不匹配143SIGTERM被停止信号终止通常是手动 stop 或系统关机触发遇到 137 就要重点排查内存限制。-m参数设置了内存上限堆内存大的 Java 应用特别容易撞上限。这时候不只是调大限制还要检查应用自身的内存参数两头都调才可能解决问题。7.2 日志膨胀与空间不足容器日志默认写到宿主机如果应用疯狂输出日志文件会无节制增长最终把磁盘撑爆。这个坑基本每个用 Docker 的人都会遇到。策略有三层在 Compose 里配置日志轮转services: app: logging: driver: json-file options: max-size: 10m max-file: 3定期清理文件docker system prune -f这个命令会清理悬空镜像、停止的容器、无用的网络和构建缓存能回收不少空间。应用侧控制日志输出级别把业务日志和访问日志分流。这是根本解法靠容器层轮转只是兜底。7.3 时区问题官方镜像默认时区多是 UTC日志时间比北京时间晚 8 小时排查问题时非常容易误判。标准做法是在 Dockerfile 里加ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone但注意基础镜像如果太精简可能连tzdata都没装上面这步会失败。这时候要先apt-get install -y tzdata或apk add tzdata。7.4 镜像内缺命令的调试技巧容器镜像为了控制体积往往裁掉了大量调试工具。连vi、curl、ping都没有。临时调试的方法是让容器共享宿主进程网络栈然后直接在宿主机上访问docker run --rm -it --network container:web nicolaka/netshootnetshoot镜像内置了 curl、tcpdump、dig 等各种网络工具把它挂到目标容器的网络命名空间里就能用宿主机容器化的方式做排查。排查完容器直接删除对目标环境零污染。7.5 镜像构建缓存失效的隐性原因构建缓存失效并不只是因为你改了代码。文件时间戳也会影响缓存命中。COPY指令比较的是文件内容校验和不是时间戳所以一般没问题。但有些外部依赖比如apt-get update每次执行都不会命中同一层缓存因为源列表的更新时间在变化。对这些操作可以在指令前加--mounttypecache做构建缓存挂载或者接受每次重新拉包的现实不用过度优化。个人实际用的最大体会是Docker 的知识点看着零散其实核心脉络非常清晰——镜像决定你跑什么容器决定你怎么跑数据卷和网络决定怎么持久、怎么通信Compose 把这一切编排起来。把这四条线捋顺再复杂的环境问题都能拆解成可排查的小问题。最后再分享一个小技巧多读几次docker inspect的输出它是 Docker 各种问题的“案发现场”比任何教程都实在。