☰
Docker入门:容器不是虚拟机,理解镜像与数据卷是关键
2026/10/12 14:25:17 网站建设 项目流程

简介:《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 之后容器进程在前台跑,Ctrl+C 一按容器就停了,这就是“容器生命周期等于主进程生命周期”的体现。

这个机制也解释了容器为什么不建议塞多个服务。容器设计的主基调是每个容器只跑一个前台进程,多个服务之间通过容器网络互相访问。如果你在一个容器里同时跑 nginx 和数据库,主进程退出后整个容器都要跟着停,另一个组件即使进程还活着也被一并终止。所以入门阶段就要养成分工意识:一个容器只负责一件事,关联组件用多个容器编排定义在一块,让启动顺序和网络依赖明确可控。

那种“进去装个 SSH 再说”的做法更要避免。Docker 容器不是给你登录用的服务器,SSH 进去不但多一个进程让容器退出逻辑更复杂,还扩大了暴露面。调试容器用 docker exec,不是 SSH;要固化调整,就写 Dockerfile 重新构建镜像,不要依赖人工修改容器内部。

2.3 用 docker inspect 和 docker diff 拆一次现场

概念讲到这里,可以亲手验证一次。先把一个基础容器跑起来,然后用两条命令拆它的运行现场:

docker run -d --name=web-demo nginx:alpine docker diff web-demo docker container inspect web-demo

docker run -d 让容器在后台运行;--name=web-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 $USER

docker --version 确认客户端能拿到 Docker 版本号,如果这个命令失败,先查环境变量 DOCKER_HOST 是否指向了别处;systemctl enable --now docker 让 Docker 守护进程开机自启,服务器上如果漏掉这一步,重启后所有容器都不会自动拉起,服务直接断掉;usermod -aG docker 把当前用户加入 docker 用户组,之后 docker 命令不用每次都加 sudo,注意重新登录终端才能生效。如果在 root 环境或者团队统一用 sudo,这步可跳过,但要记住“用户权限不足”也是 docker ps 报错的一个原因,不是所有报错都来自守护进程。

接着再看一眼 docker context:

docker context ls

这条命令列出当前 Docker 客户端连接的所有环境,正常情况下会有 default 一项。我见过有人本地装的 Docker,跑起来一切正常,但 docker context 已经被切到远端环境,导致镜像列表和实际控制的对象不是同一套,排查半天才发现问题。所以每次到一个新环境,先看 context 再继续,这个习惯成本极低,收益却很高。

3.2 用 Nginx 验证端口映射:-p 与 -v 的第一次配合

第一个业务容器选 Nginx 最合适,它启动稳定、日志清晰,还能一次性演示端口映射和文件挂载两个核心参数。先准备一个本地目录放测试页面,再启动容器:

mkdir -p "$HOME/docker-demo/html" echo '<h1>hello docker</h1>' > "$HOME/docker-demo/html/index.html" docker run -d \ --name=web-demo \ -p 8080:80 \ -v "$HOME/docker-demo/html:/usr/share/nginx/html:ro" \ nginx:alpine

mkdir 和 echo 两行是准备宿主机文件,docker run 才是核心。-d 表示后台运行,不会把日志直接打在终端上;--name=web-demo 是容器名;-p 8080:80 把宿主机 8080 端口映射到容器内 80 端口;-v 把宿主机目录挂载为容器内 Nginx 的网页目录,:ro 表示只读挂载。挂载用绝对路径,避免相对路径在不同工作目录下产生语义差异。

curl http://127.0.0.1:8080 docker logs --tail 10 web-demo

curl 访问本地 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 -a

stop 给容器主进程发送 SIGTERM 信号,预留宽限时间后再发 SIGKILL,容器并没有被删除,只是进程被终止;start 会把同一容器重新启动,端口映射、挂载配置、数据卷都不会丢;restart 是先停再启,适合修改参数或环境后快速让进程重新加载。docker ps -a 看清所有容器包括已停止的,很多入门者习惯只看 docker ps,长期运行的容器一旦退出,不加 -a 就看不到,排查时遗漏一半信息。

容器内部操作主要用 exec 和 logs:

docker exec -it web-demo sh docker logs -f --tail 20 web-demo

docker exec 在运行中的容器里再起一个进程,-it 组合表示交互式终端,sh 是打开 shell。在容器里可以看文件、查进程、试网络,但注意这个 shell 是独立进程,退出不影响容器主进程。docker logs -f --tail 20 持续跟踪容器日志,tail 20 只显示最近 20 行,适合排障时实时观察输出。这里很多人用错 attach:attach 连接的是容器主进程的输入输出,如果主进程不是交互式 shell,attach 进去很容易卡住黑屏,调试时优先用 exec。

资源限制不建议放在“以后再说”的清单里,它应该是容器启动参数的一部分:

docker run -d \ --name=web-demo \ --memory=256m \ --cpus=0.5 \ --restart=always \ -p 8080:80 \ nginx:alpine

--memory=256m 限制容器最多使用 256MB 内存,超过阈值可能触发 OOM 被杀;--cpus=0.5 限制容器最多使用半个 CPU 核心,避免一个异常进程吃满整机;--restart=always 表示容器异常退出或 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 TZ=Etc/GMT+8。这种方式不改镜像内容,调整参数重启容器就能生效,适合入门阶段使用。如果项目里多个容器都要统一时区,建议直接写进 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.txt,RUN 安装依赖,最后再 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 数组而不是字符串,容器主进程能直接接住 SIGTERM,docker stop 时反应更干净。EXPOSE 只是声明,实际端口映射仍由 docker run -p 决定。

构建与验证:

docker build -t demo-app:v1 . docker image inspect demo-app:v1

docker 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_ENABLED=0 go build -o /app/server . # 第二阶段:干净运行环境,只保留产物 FROM debian:stable-slim COPY --from=build /app/server /usr/local/bin/server EXPOSE 8080 CMD ["server"]

第一阶段以 AS build 命名,第二阶段 COPY --from=build 从该阶段取文件,这是多阶段构建的关键两行。最终镜像里没有源码、没有编译工具、没有中间缓存,只有二进制文件,体积通常能缩到原来的四分之一到五分之一。对入门者来说,即使暂时没有编译项目,也要理解这个模式,因为它让“构建环境”和“运行环境”解耦,Dockerfile 本身变成一套完全自动化的交付流水线。整个章节走下来,你会发现 Dockerfile 解决的不只是打包问题,它把环境从某个人脑子里的经验,变成了一段别人也能照着构建的描述。这就是“可复现交付”的价值。

6. 容器跑起来之后:十分钟自检与最值得养成的启动习惯

6.1 先记住这条检查顺序:ps、logs、stats,还不够就 inspect

容器全部调通以后,把下面这组检查固化成本能,它比任何监控工具都先一步发现问题。每次接手一个容器环境,都按这个顺序走一遍:

docker ps -a docker logs --tail 20 容器名 docker stats --no-stream

docker ps -a 看容器是否在预期状态;docker logs 看最近有没有报错堆栈;docker stats 不带 --no-stream 会持续刷新占用率,带 --no-stream 只输出当前快照,适合脚本化调用。如果三条命令还不够定位,再补一条 docker inspect 看详细配置和网络信息。这套组合不依赖外部平台,在任何 Linux 机器上都能执行,覆盖入门阶段八成的排障场景。

给长期运行的容器设置参数时,我最低限度会加三样:--restart=always 让守护进程重启后自动拉起容器,--memory 和 --cpus 限制资源占用,同时给日志驱动加上单文件大小上限。有人觉得这些参数增加了启动命令的复杂度,但事实证明,服务器重启后服务静默下线、日志撑爆磁盘这类事故,恰恰都发生在省略了最小参数的容器上。

讲一个我自己的教训:有一段时间我喜欢把 docker run 命令随手敲完就跑,不存脚本也不写注释。后来服务器重启,依赖容器的服务全部没起来,我对着终端回忆当初到底用了哪些参数,只记得端口,其他一概想不起来。从那以后我的习惯是,任何一个要长期跑的容器,先把它完整的 docker run 参数写成启动脚本,放进团队环境管理目录;参数有调整,同步改脚本和容器。容器本身不保存你的历史操作,命令也不写进日志,你的操作记录才是唯一靠得住的后悔药。这个习惯花不了两分钟,但换机器、换人接手时价值巨大。希望这套 Docker 基础梳理,能帮你少经历几次我当时的慌乱,也希望你把容器用在真正该用的地方:让环境交付可复现,而不是为了用而用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询