ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Docker入门:容器不是虚拟机,理解镜像与数据卷是关键

Docker入门:容器不是虚拟机,理解镜像与数据卷是关键 简介《Docker基础入门指南》是一份面向容器技术初学者的便携式学习文档尤其适合已有一定计算机基础、希望借助容器化手段提升开发部署效率的开发者。文档先从容器与虚拟机的差异讲起帮助读者理解为什么容器更轻量、启动更快随后系统梳理镜像、容器、仓库三大核心概念并分别给出在 Windows、macOS 以及 Ubuntu 系统上的安装方法。基础操作部分覆盖拉取镜像、运行容器、查看容器状态、进入容器内部、停止与删除容器等常用命令形成完整的使用链路。实战环节包含两个典型案例一是利用 Nginx 镜像快速部署 Web 服务二是通过编写 Dockerfile 创建 Python 应用镜像让读者体验从构建到运行的完整流程。此外还整理了权限不足、镜像下载缓慢、容器数据持久化等高频问题的处理方法并提供官方文档、互动教程等拓展学习资源。针对数据卷挂载、镜像加速等易错点也给出了清晰的操作建议可有效降低初学者试错成本。资源包为单个 PDF 文件大小约 696KB便于离线学习目前已有 126 人学习下载。1. Docker 基础入门最该先丢掉错觉容器不是轻量虚拟机Docker 基础入门最容易被带偏的地方是把容器当成一台能随便登录的轻量虚拟机。我第一次这么想的时候很快撞上三件怪事容器里改的文件在 docker rm 之后消失端口明明映射了外部却访问不通时间日志永远比宿主机慢 8 小时。后来才明白入门的第一步不是背 docker run而是理解镜像、容器、数据卷分别承担什么职责。下文按一条完整路径展开先看懂 Docker 的分层思想再跑通第一个业务容器拆 5 个最常见的踩坑点最后用 Dockerfile 把自己的服务变成可复现的镜像。适合刚接触容器、想尽快在服务器上稳定交付服务的开发者也适合负责团队环境维护的一线人员。读完你应该能独立把服务容器化并知道出问题时先查哪一层。2. 搞清镜像和容器的分工先看懂分层的黑匣子再敲命令2.1 镜像为什么是只读的容器却能随便写镜像本质上是一组只读文件系统层的叠加每一层记录一次变更。Docker 启动容器时会在镜像顶层叠一个可写层运行时写入全部落在这里读操作则按需从镜像层读取。写时复制是这个模型的精髓初次写某个文件前先把文件从镜像层复制到可写层再执行修改。这样同一个镜像可以同时支撑几十个容器实例底层文件系统共享互不覆盖容器启动开销远小于虚拟机。这种设计带来的最直接后果是容器的可写层和容器实例同生共死。你在容器里创建的文件、安装的工具、改过的配置只要没有挂载数据卷都放在可写层docker rm 之后一并被清理。很多入门者第一次翻车就在这跑了个临时容器进去装环境、写脚本隔天不小心 docker rm数据就没了。这不是 Docker 的缺陷而是它的运行模型本来就是这样数据要想活下来必须放到镜像、数据卷或宿主机挂载目录里去。docker pull nginx:alpine docker image inspect nginx:alpine第一条命令把 Nginx 的 Alpine 版本拉到本地Alpine 是体积较小的 Linux 发行版镜像只有几十 MB适合入门实验。第二条命令查看镜像的完整元数据重点看 DiffIds 字段这个数组里每一项对应一层文件系统数组越长镜像层数越多。inspect 输出很长建议配合 grep 只看关键字段比如用docker image inspect nginx:alpine | grep -A 10 DiffIds这样能把分层关系看得更清楚。2.2 容器不是小虚拟机它只是被隔离的一组进程理解容器的一个关键纠正容器内部没有完整操作系统它和宿主机共用内核只是通过命名空间把进程、网络、挂载点、文件系统视图隔离成独立空间。容器里用 ps 只能看到自己的进程网络栈也是独立一套但别再期待容器里有 systemd 或者能像虚拟机一样提供从内核到发行版的完整环境。回想第一次启动容器时的感受docker run 之后容器进程在前台跑CtrlC 一按容器就停了这就是“容器生命周期等于主进程生命周期”的体现。这个机制也解释了容器为什么不建议塞多个服务。容器设计的主基调是每个容器只跑一个前台进程多个服务之间通过容器网络互相访问。如果你在一个容器里同时跑 nginx 和数据库主进程退出后整个容器都要跟着停另一个组件即使进程还活着也被一并终止。所以入门阶段就要养成分工意识一个容器只负责一件事关联组件用多个容器编排定义在一块让启动顺序和网络依赖明确可控。那种“进去装个 SSH 再说”的做法更要避免。Docker 容器不是给你登录用的服务器SSH 进去不但多一个进程让容器退出逻辑更复杂还扩大了暴露面。调试容器用 docker exec不是 SSH要固化调整就写 Dockerfile 重新构建镜像不要依赖人工修改容器内部。2.3 用 docker inspect 和 docker diff 拆一次现场概念讲到这里可以亲手验证一次。先把一个基础容器跑起来然后用两条命令拆它的运行现场docker run -d --nameweb-demo nginx:alpine docker diff web-demo docker container inspect web-demodocker run -d 让容器在后台运行--nameweb-demo 给容器一个固定名字后面的命令都能直接引用它nginx:alpine 是启动 Nginx 的轻量镜像入口进程简单适合观察。docker diff 会列出容器可写层相对镜像的变更A 表示新增、C 表示修改、D 表示删除。一个刚启动的 Nginx 容器也能看到几条 A 开头的记录因为这些是 nginx 运行时的临时文件正好印证了可写层的存在。docker container inspect 返回容器运行时配置重点看 NetworkSettings 下面的 IPAddress、Ports、Gateway 字段。容器自己的 IP、网关、端口映射都挂在这里后面章节讲宿主机访问容器时为什么 -p 映射能生效答案就在这一份 JSON 里。看到这些输出后应该对 Docker 的三个基本对象有了体感镜像负责交付容器负责运行数据卷负责保存状态。后续所有命令和坑都绕不开这三角关系。把以上理解固定住再进入端口映射和挂载的实际操作。3. 跑通第一个业务容器端口映射、挂载和生命周期3.1 装完 Docker 先别急着跑服务确认三件事很多教程装完 Docker 直接 docker run跑出结果就结束结果换一台机器或者重启服务器问题全部暴露。这里先花五分钟做三个基础确认分别是版本、自启、用户权限。docker --version sudo systemctl enable --now docker sudo usermod -aG docker $USERdocker --version 确认客户端能拿到 Docker 版本号如果这个命令失败先查环境变量 DOCKER_HOST 是否指向了别处systemctl enable --now docker 让 Docker 守护进程开机自启服务器上如果漏掉这一步重启后所有容器都不会自动拉起服务直接断掉usermod -aG docker 把当前用户加入 docker 用户组之后 docker 命令不用每次都加 sudo注意重新登录终端才能生效。如果在 root 环境或者团队统一用 sudo这步可跳过但要记住“用户权限不足”也是 docker ps 报错的一个原因不是所有报错都来自守护进程。接着再看一眼 docker contextdocker context ls这条命令列出当前 Docker 客户端连接的所有环境正常情况下会有 default 一项。我见过有人本地装的 Docker跑起来一切正常但 docker context 已经被切到远端环境导致镜像列表和实际控制的对象不是同一套排查半天才发现问题。所以每次到一个新环境先看 context 再继续这个习惯成本极低收益却很高。3.2 用 Nginx 验证端口映射-p 与 -v 的第一次配合第一个业务容器选 Nginx 最合适它启动稳定、日志清晰还能一次性演示端口映射和文件挂载两个核心参数。先准备一个本地目录放测试页面再启动容器mkdir -p $HOME/docker-demo/html echo h1hello docker/h1 $HOME/docker-demo/html/index.html docker run -d \ --nameweb-demo \ -p 8080:80 \ -v $HOME/docker-demo/html:/usr/share/nginx/html:ro \ nginx:alpinemkdir 和 echo 两行是准备宿主机文件docker run 才是核心。-d 表示后台运行不会把日志直接打在终端上--nameweb-demo 是容器名-p 8080:80 把宿主机 8080 端口映射到容器内 80 端口-v 把宿主机目录挂载为容器内 Nginx 的网页目录:ro 表示只读挂载。挂载用绝对路径避免相对路径在不同工作目录下产生语义差异。curl http://127.0.0.1:8080 docker logs --tail 10 web-democurl 访问本地 8080 端口如果能返回刚才写的 hello docker 页面说明端口映射和文件挂载都生效了。docker logs 看容器标准输出Nginx 访问日志默认打到标准输出这条命令能看到刚才访问产生的 200 状态码。如果你机器上没装 curl也可以浏览器直接开 127.0.0.1:8080但排障时浏览器可能受缓存之类因素干扰不如 curl 直接、干净。挂载参数有几个易错点。第一容器启动后如果挂载的目录不存在Docker 会自动创建目录但某些镜像对目录权限敏感建议提前 mkdir第二挂载单个文件时宿主机文件必须存在否则容器会启动失败第三:ro 和:rw 要写在容器路径后面顺序是 /宿主机路径:/容器路径:ro不要写成 /宿主机路径:ro:/容器路径。多跑两次后这些细节自然会变成肌肉记忆。3.3 容器生命周期stop 不是删除exec 不是 SSH容器跑起来之后日常操作从 docker run 变成下面一组命令docker stop web-demo docker start web-demo docker restart web-demo docker ps -astop 给容器主进程发送 SIGTERM 信号预留宽限时间后再发 SIGKILL容器并没有被删除只是进程被终止start 会把同一容器重新启动端口映射、挂载配置、数据卷都不会丢restart 是先停再启适合修改参数或环境后快速让进程重新加载。docker ps -a 看清所有容器包括已停止的很多入门者习惯只看 docker ps长期运行的容器一旦退出不加 -a 就看不到排查时遗漏一半信息。容器内部操作主要用 exec 和 logsdocker exec -it web-demo sh docker logs -f --tail 20 web-demodocker exec 在运行中的容器里再起一个进程-it 组合表示交互式终端sh 是打开 shell。在容器里可以看文件、查进程、试网络但注意这个 shell 是独立进程退出不影响容器主进程。docker logs -f --tail 20 持续跟踪容器日志tail 20 只显示最近 20 行适合排障时实时观察输出。这里很多人用错 attachattach 连接的是容器主进程的输入输出如果主进程不是交互式 shellattach 进去很容易卡住黑屏调试时优先用 exec。资源限制不建议放在“以后再说”的清单里它应该是容器启动参数的一部分docker run -d \ --nameweb-demo \ --memory256m \ --cpus0.5 \ --restartalways \ -p 8080:80 \ nginx:alpine--memory256m 限制容器最多使用 256MB 内存超过阈值可能触发 OOM 被杀--cpus0.5 限制容器最多使用半个 CPU 核心避免一个异常进程吃满整机--restartalways 表示容器异常退出或 Docker 守护进程重启后自动拉起这是业务容器最低限度的自愈配置。不加这三项容器短时间可能一切正常但首次故障或重启时你会付出比配置参数大得多的救火代价。4. 容器化落地最容易踩的五个坑现象、原因与排查命令4.1 镜像拉不下来、拉到一半卡住现象docker pull 一直停在某个层或者报 TLS 握手失败、超时换一个镜像名也一样。原因有三层网络到远端镜像仓库不稳定DNS 解析异常以及宿主机网卡 MTU 过大导致数据包在特定网络环境里被丢弃。前两种容易想到第三种最隐蔽它的典型表现是普通网页访问正常但镜像拉取一直断断续续原因是传输较大数据块时不断分片失败。解决先做基础排查再动手改配置。用cat /etc/resolv.conf看 DNS用ip link看当前网卡 MTU如果发现 MTU 确实偏大把 Docker 桥接网络的 MTU 调小并重启 Docker 服务。如果 DNS 和 MTU 都正常那大概率是镜像源本身不稳定可以在/etc/docker/daemon.json里配置 registry-mirrors填入团队内部已部署的镜像源或机构维护的镜像仓库然后执行sudo systemctl restart docker。没有内部源的场景就错峰重试同时避免在每次 docker run 时才拉新镜像提前 pull 到本地是一个稳定做法。这个坑越早解决越省心否则后续每个 docker run 都可能卡一遍。4.2 容器一删数据和配置全没了现象容器运行过程中写入的文件、安装的工具、调整过的配置docker stop 再 start 之后还在但 docker rm 之后重新 docker run 一个同名容器一切归零。原因很基础容器可写层绑定容器实例docker rm 删除的是整个运行时实例可写层跟着回收。你想保留的任何数据都必须存放在镜像之外的地方容器只负责运行。解决启动时就挂载数据卷或宿主机目录。推荐具名卷先docker volume create demo-data再docker run -d -v demo-data:/var/lib/nginx nginx:alpine这样日志或上传文件都能写到具名卷里容器删除后卷还在下次用相同卷名挂载数据就找回来了。临时测试可以用匿名卷但匿名卷在容器删除后残留成孤儿卷长期堆积会占磁盘空间。判断规则很简单凡是“删了容器会后悔没留”的数据启动参数里就应该已经挂载好了不要指望事后能找回后悔药只在 docker run 那一刻存在。4.3 容器日志时间比宿主机慢 8 小时现象容器应用打印的日志时间戳比宿主机当前时间慢 8 小时定时任务也总在预期之外的时间触发。原因单一明确容器默认时区是 UTC宿主机通常是东八区两边差 8 小时。这个坑之所以普遍是因为时间同步在其他层面通常正常你不会第一时间怀疑容器时区。解决启动容器时把宿主机时区文件挂进去-v /etc/localtime:/etc/localtime:ro同时通过环境变量设置时区-e TZEtc/GMT8。这种方式不改镜像内容调整参数重启容器就能生效适合入门阶段使用。如果项目里多个容器都要统一时区建议直接写进 Dockerfile 的基础层减少每次启动时重复传参。应用本身如果和部署环境有跨时区协作需求更稳妥的做法是在代码里显式指定时区而不是读系统时区。排查时先执行docker exec -it 容器名 date看容器内时间再对比宿主机 date如果差值固定是 8 小时直接定位为容器时区配置问题不用再折腾应用代码。4.4 端口映射了但外部就是访问不通现象curl 127.0.0.1:8080 能访问docker ps 也能看到 0.0.0.0:8080-80/tcp但换成机器 IP 从外部访问就是超时。原因通常分三层启动参数把映射写成了-p 127.0.0.1:8080:80导致端口只监听在回环地址外部不可能访问宿主机防火墙拦截机器前面的安全组或虚拟化网络策略没放行。解决按顺序排查。先执行docker port web-demo看映射绑定的地址如果是 127.0.0.1:8080-80/tcp说明你想对外开放就得改成-p 0.0.0.0:8080:80重新创建容器。再用ss -tnlp | grep 8080查宿主机实际监听状态确认端口真的被 Docker 占用。如果映射和监听都正常再用本机 curl 与局域网另一台机器 curl 做对照外部访问不通的问题就落在防火墙或安全组上。云服务器实例的安全组规则里需要放行对应端口这是容器之外的链路docker ps 看不出任何异常。排查这类问题最忌讳只盯着 docker ps映射在上层是否被拦截它完全不知情。4.5 容器里少了一堆常用命令排障像在裸奔现象docker exec 进入容器想用 curl 测试接口报 command not found想改个配置文件连 vim 也没有。原因不是镜像坏了而是基础镜像刻意保持最小化只装运行应用所必需的依赖这是减小体积、降低暴露面的常规做法。把生产容器当一台服务器登录使用是容器习惯里最需要纠正的一条。解决优先在宿主机侧排障docker logs 和 docker inspect 能帮你拿到大部分信息没必要先进容器。确需进入容器观察时临时补装工具可以用apt-get update apt-get install -y curl但工具只在当前容器的可写层存在容器删除后一切归零不要依赖这种操作。更推荐的方案是临时启动一个带调试工具的容器共享目标容器的网络命名空间docker run -it --rm --network container:web-demo 你信任的调试镜像。这条命令让两个容器共用网络栈你在调试容器里看到的端口和网络状态就是目标容器的同时业务镜像保持原样最小化。平时准备一个团队通用的调试镜像按需拉取即可不要往业务镜像里塞一堆不常用的工具镜像体积和排障便利之间怎么平衡入门阶段至少要知道还有这条选项。5. 用 Dockerfile 把服务变成可复现的镜像指令、缓存与瘦身5.1 指令顺序是有代价的每一层都在影响缓存命中很多人第一次写 Dockerfile 是照抄模板FROM、RUN、COPY 顺序随意结果每次改代码都要重新下载一样依赖构建时间被拖长。想理解为什么需要把 Dockerfile 的每条指令当成一层镜像层。重新构建时Docker 会逐层判断输入是否变化没有变化就复用这一层以及后面所有能复用的层某一步输入发生变化这一层和它之后的所有层都要重新构建。所以指令顺序的核心原则是把变化频率低的放在前面变化频率高的放在后面。以 Python 服务为例requirements.txt 里的依赖列表相对稳定业务代码则频繁修改。正确顺序是先 COPY requirements.txtRUN 安装依赖最后再 COPY 业务代码。这样每次改业务代码依赖安装层被全部复用构建时间能缩短一个量级。反过来第一行 COPY . . 会把整个目录带进去任何文件变化都会让后面所有指令重跑。这个优化不需要额外工具只需要调整指令顺序是所有 Dockerfile 的通用加速手段。5.2 一个最小而完整的 Python 服务镜像从 Dockerfile 到构建看一个可以直接照跑的 Dockerfile假设服务是一个监听 8000 端口的 Python 应用# 选择 slim 版本保留常用系统组件但去掉大量不必要内容 FROM python:3.11-slim # 统一工作目录后面指令都基于这个目录 WORKDIR /app # 先复制依赖清单利用层缓存减少重复安装 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 业务代码最后复制改动频繁但不会触发依赖层重建 COPY . . # 声明容器内端口不是自动映射仅供文档和 --link 场景识别 EXPOSE 8000 # 主进程是 python main.py容器退出和应用退出同步 CMD [python, main.py]每一段都有注释构建时终端里的 Step 编号就是每一层。python:3.11-slim 比默认 Python 镜像少了大量编译工具和文档体积小很多WORKDIR /app 把所有相对路径锚定在这一个目录COPY requirements.txt . 单独成层业务代码还没进来依赖层基本稳定RUN pip install --no-cache-dir 关闭 pip 缓存防止缓存文件进镜像这是镜像瘦身里见效最快的一行CMD 用 JSON 数组而不是字符串容器主进程能直接接住 SIGTERMdocker stop 时反应更干净。EXPOSE 只是声明实际端口映射仍由 docker run -p 决定。构建与验证docker build -t demo-app:v1 . docker image inspect demo-app:v1docker build 最后的点指定构建上下文为当前目录Docker 会把这个目录整个打包发给守护进程所以构建目录不宜太大。构建完用 docker image inspect 看 Size 字段记下这个基础值下一节做多阶段构建时对比效果会非常直观。5.3 多阶段构建与 .dockerignore镜像瘦身的两个基本动作先看 .dockerignore它和 .gitignore 目的一样告诉 Docker 构建时哪些文件和目录不进入构建上下文.git __pycache__ *.pyc .env venv/ *.md把版本信息、缓存、虚拟环境、密钥文件全部排除构建上下文更小更重要的是防止本地私密文件被烧进镜像。很多事故不是运行环境被攻破而是镜像里带着 .env 或数据库备份被推到了镜像仓库。多阶段构建适合需要编译的项目做法是在一个带编译器的镜像里完成构建再用一个干净的轻量镜像只保留运行产物。以 Go 服务为例# 第一阶段带完整工具链负责编译 FROM golang:1.21-slim AS build WORKDIR /src COPY . . RUN CGO_ENABLED0 go build -o /app/server . # 第二阶段干净运行环境只保留产物 FROM debian:stable-slim COPY --frombuild /app/server /usr/local/bin/server EXPOSE 8080 CMD [server]第一阶段以 AS build 命名第二阶段 COPY --frombuild 从该阶段取文件这是多阶段构建的关键两行。最终镜像里没有源码、没有编译工具、没有中间缓存只有二进制文件体积通常能缩到原来的四分之一到五分之一。对入门者来说即使暂时没有编译项目也要理解这个模式因为它让“构建环境”和“运行环境”解耦Dockerfile 本身变成一套完全自动化的交付流水线。整个章节走下来你会发现 Dockerfile 解决的不只是打包问题它把环境从某个人脑子里的经验变成了一段别人也能照着构建的描述。这就是“可复现交付”的价值。6. 容器跑起来之后十分钟自检与最值得养成的启动习惯6.1 先记住这条检查顺序ps、logs、stats还不够就 inspect容器全部调通以后把下面这组检查固化成本能它比任何监控工具都先一步发现问题。每次接手一个容器环境都按这个顺序走一遍docker ps -a docker logs --tail 20 容器名 docker stats --no-streamdocker ps -a 看容器是否在预期状态docker logs 看最近有没有报错堆栈docker stats 不带 --no-stream 会持续刷新占用率带 --no-stream 只输出当前快照适合脚本化调用。如果三条命令还不够定位再补一条 docker inspect 看详细配置和网络信息。这套组合不依赖外部平台在任何 Linux 机器上都能执行覆盖入门阶段八成的排障场景。给长期运行的容器设置参数时我最低限度会加三样--restartalways 让守护进程重启后自动拉起容器--memory 和 --cpus 限制资源占用同时给日志驱动加上单文件大小上限。有人觉得这些参数增加了启动命令的复杂度但事实证明服务器重启后服务静默下线、日志撑爆磁盘这类事故恰恰都发生在省略了最小参数的容器上。讲一个我自己的教训有一段时间我喜欢把 docker run 命令随手敲完就跑不存脚本也不写注释。后来服务器重启依赖容器的服务全部没起来我对着终端回忆当初到底用了哪些参数只记得端口其他一概想不起来。从那以后我的习惯是任何一个要长期跑的容器先把它完整的 docker run 参数写成启动脚本放进团队环境管理目录参数有调整同步改脚本和容器。容器本身不保存你的历史操作命令也不写进日志你的操作记录才是唯一靠得住的后悔药。这个习惯花不了两分钟但换机器、换人接手时价值巨大。希望这套 Docker 基础梳理能帮你少经历几次我当时的慌乱也希望你把容器用在真正该用的地方让环境交付可复现而不是为了用而用。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进