☰
Docker实战笔记:从镜像、容器到Compose编排的完整学习指南
2026/10/19 11:24:46 网站建设 项目流程

去年秋天,我报名参加了一个开源学习社区组织的Docker专项学习活动,从安装环境一路学到用Compose编排多容器应用,前后持续了将近一个月。报名之前,我在日常开发里已经零散用过一些Docker命令,但状态很尴尬——能照着教程把容器跑起来,却讲不清镜像层、容器层、数据卷之间的关系;遇到容器一停数据就丢、镜像体积撑到好几个G、多个服务互相连不通这类问题,依然只能靠搜索引擎临时救火。这篇博客记录的,就是这趟学习之旅里我真正想明白的东西:Docker的核心概念如何用"人话"理解、第一个项目怎么容器化、哪些坑几乎人人都会踩,以及从"能用命令"到"会设计镜像"需要跨过哪些坎。如果你是完全零基础的新人,或者跟我一样处在"半懂不懂"的状态,这篇笔记应该能帮你省掉不少弯路。

1. 报名之前:我对Docker的"会用"其实是一种假象

1.1 三个让我当场卡住的真实场景

先说报名前的工作状态。团队发版一直沿用手工打包加脚本上传的流程,每次上线都像一场接力赛:有人负责编译,有人负责传包,有人负责重启服务。机器环境稍有风吹草动,比如某台机器缺了底层依赖库,整套流程就会卡在最后一步。我那时候已经意识到容器化的价值:它能把依赖、配置、代码全部固化到一个镜像里,在任何机器上跑出同样的结果。这是解决"环境不一致"问题的关键思路。

但真正促使我去系统学习的,是三个让我当场就卡的场景。第一个场景:我需要在容器里修改Nginx配置,进容器用了vim改完,重启容器之后配置全没了,当时我完全想不通为什么。第二个场景:两个项目分别依赖不同版本的Node,我想用Docker拉起两个隔离环境,却不清楚端口和网络到底该怎么规划。第三个场景:同事发来一份镜像构建脚本,看起来非常简单,可docker build执行到一半就报错,我一时分不清是项目代码问题、网络问题还是Dockerfile写错了。

这三个场景的共同点在于:我记住了命令的"形",却没理解命令背后的"义"。docker run会启动容器、docker ps会列出容器,这些我当然知道,但容器是什么、生命周期受什么控制、可写层和数据卷有什么关系,这些问题我根本无法回答。带着这些模糊地带去排查问题,就像蒙着眼睛修电路,只能靠乱试碰运气。

1.2 社区化学习跟一个人看教程的区别

后来朋友推荐我加入这个开源学习社区,它的学习方式和我以前自己刷教程完全不同。社区有一套明确的知识地图:从安装环境开始,依次是镜像、容器、数据卷、网络,再到Dockerfile、Compose,每一步都有必须完成的任务。课程不是"看完就算"的视频课,每一章都有需要实际动手的作业,跑通了要把结果截图发到社区里,还会有人互相点评。这种机制最大的好处,就是你根本没办法假装自己会了——因为跑不出结果就是跑不出结果。

另一个明显的差异在问题讨论的深度。同一个报错信息,在不同人的机器上可能呈现出完全不同的现象,原因也五花八门。我在社区里看过十几个人讨论同一道题的多种解法、同一个坑的不同表现之后,对一个知识点的理解深度会远超独自看书。这也是为什么我后来一直建议身边的人,学容器化技术不要只对着文档敲,找一个有反馈、有同伴的圈子,进步速度会完全不一样。

2. 镜像、容器和仓库:核心概念必须用"人话"想明白

2.1 镜像不是虚拟机快照,是分层的只读模板

很多初学者(包括当初的我)会把Docker镜像理解成"一个迷你版操作系统",这个类比其实会误导人。更准确的理解是:镜像是分层的只读模板,每一层对应一组文件系统的变化。用生活化的例子打比方,镜像像一套乐高套装——说明书加全套零件,容器是你照着说明书拼出来的那个成品。同一套零件能拼出无数个一模一样的成品,你还可以在成品上继续加自己的想法。

这个"分层"的概念极其重要,因为它直接影响构建速度和镜像体积。Dockerfile里每一条RUN、COPY指令都会生成一个新的镜像层,Docker在构建时会尽量复用已有的层,也就是所谓的缓存。如果你改动了某一层相关的文件,从那之后的所有层都要重新生成。许多人把多个RUN合并成一条命令以减少层数,这个做法本身是对的,但不理解原理就只会模仿,一旦遇到缓存失效的问题仍然无从下手。

2.2 容器是可写实例,数据卷是容器外面的储物柜

镜像是只读的,容器则在镜像之上凭空多了一层可写层。你可以在容器里安装软件、修改配置、创建文件,但这些改动默认全部落在容器层。问题在于,容器层的生命周期和容器完全绑定:容器一旦被删除,可写层也会跟着被抹掉。这就是"容器一停数据就丢""容器一删配置全没"的根本原因。

要持久化保存数据,就得靠数据卷。数据卷是独立于容器生命周期存在的存储空间,你可以把它挂载到容器内的某个目录,让程序把需要长期保留的数据写入这个卷。即使删除容器再重新创建,只要挂载同一个卷,数据依然还在。实际开发里还有一种做法叫绑定挂载,把宿主机上的目录直接映射进容器,我经常在开发阶段用它来做代码热更新,改一行代码,容器里立刻就能感应到,不用反复重建镜像。

2.3 镜像仓库与网络:镜像从哪来,容器之间怎么通信

镜像仓库是存放和分发镜像的地方,平时执行docker pull就是从仓库拉取镜像,docker push则是把本地镜像上传到仓库。公共仓库在部分网络环境下拉取非常慢,解决方案一般有两个方向:一是配置一台访问链路更通畅的镜像源地址,二是在内网搭建私有仓库来分发镜像。学习阶段配置好镜像源就够用了,这个坑后面我会专门展开说。

容器网络方面,默认情况下容器跑在一个叫bridge的桥接网络里,每个容器有自己的内部IP。如果你启动容器时加了-p参数,那其实是在宿主机和容器之间建立了一条端口通道,外部请求先到达宿主机的某个端口,再被转发进容器的对应端口。需要多个容器互相通信时,最好的做法是创建一个自定义网络,让容器之间用服务名互相访问,而不是记挂在动态变化的IP上。这个机制和Docker内置的DNS联动,是后面编排微服务的基础。

3. 把第一个容器化项目跑通:从环境安装到完整复盘

3.1 环境安装与基础自检

我本机用的是Windows系统,所以安装了Docker Desktop。安装过程中最容易忽略的一步是确认虚拟化功能已经开启。如果主板BIOS里没打开虚拟化,Docker Desktop装了也会无法启动,报错信息往往还模棱两可。我身边就有好几个同学卡在这一步,查了半天Docker文档,最后才发现是电脑虚拟化开关的问题。

装好之后先做基础自检,两条命令就能确认环境是否正常:

docker --version docker run hello-world

hello-world是一个非常精简的测试镜像,如果终端能打印出那句欢迎信息,说明Docker拉取镜像、创建容器、运行容器这整条链路已经通了。这里有个小小的经验:第一次就卡在这一步的人不在少数,多半是镜像拉取失败,下文第4章专门给出排查思路,这里先按下不表。

3.2 用最简单的方式跑一个自写的Web应用

环境确认无误后,我按学习进度写了第一个示例项目:一个非常简单的Python Web服务,项目目录中只有两个文件——app.py和Dockerfile。服务本身逻辑很简单,启动之后在8000端口返回一行文本。Dockerfile内容大致如下:

FROM python:3.11-slim WORKDIR /app COPY app.py . CMD ["python", "app.py"]

这里解释一下为什么这样选型。基础镜像选用python:3.11-slim而不是完整版python:3.11,因为slim版剔除了编译工具和大量用不到的组件,镜像体积小很多,拉取快、占磁盘也少。这一阶段没有引入依赖安装环节,因为示例足够简单,等后面涉及第三方库时再在Dockerfile中加入安装依赖的指令。

构建并运行:

docker build -t my-first-app . docker run -d -p 8000:8000 my-first-app

访问http://localhost:8000,页面正常展示内容。这一步看似平常,其实藏着不少信息量:-d让容器进入后台运行,-p 8000:8000把宿主机的8000端口映射到容器内的8000端口。很多新手在这里犯的操作性错误是:直接不加-d运行,把终端一关,容器立刻变成Exited状态。这是因为前台运行的容器和终端会话绑定,终端一退,进程就跟着断了。

3.3 进容器内部排障的高频三件套

跑通只是起点,真实开发里你迟早要进容器内部看日志、确认进程、检查环境变量。下面三个命令组合是最高频的:

docker ps docker logs 容器ID docker exec -it 容器ID /bin/bash

docker ps用来查看容器状态和ID,docker logs直接看容器日志,docker exec则是进入运行中容器的"门钥匙"。exec命令必须带-it参数:-i表示保持标准输入打开,-t表示分配一个伪终端。少了任何一个,进入交互模式都会出现各种奇怪问题。这个细节我第一次没注意,用了docker exec 容器ID /bin/bash,结果界面卡住无法输入,后来查资料才明白,exec的交互交互模式本来就依赖这两个参数。

4. 学习旅程里几乎人人都会踩的四个坑

4.1 镜像拉不下来?先定位阶段再调整源

第一天拉取hello-world镜像,我等了四五分钟没有动静,最后直接报超时。当时我的第一反应是检查网络,第二步才意识到可能是镜像源的问题。在部分网络环境下,Docker默认的公共仓库访问链路并不顺畅,这时候需要给Docker配置一个可用的镜像源。

排查思路是这样:先单独执行docker pull hello-world,确认是单纯拉取问题还是本地环境问题。如果拉取一直卡住或超时,多半是源的问题。解决方法是打开Docker Desktop的配置界面,找到镜像源设置项,填入一个访问路径更顺畅的公共仓库地址;习惯用命令行的话,也可以直接在daemon.json里配置"registry-mirrors"字段。配置完后重启Docker再试,通常能明显改善。提醒一句:这个操作是配置镜像源的常规运维手段,和任何网络代理工具都没关系,不要混为一谈。

4.2 容器一停数据就丢:数据卷的惨痛教训

这个坑我踩得最痛。有一次我在容器里初始化了一个数据库,灌入了一批测试数据,当时图省事没挂载数据卷。后来为了清理磁盘,我直接docker rm把容器删了,顿时傻眼——整个数据库连带所有测试数据消失得干干净净。那一刻我对"容器层生命周期随容器销毁而消失"这句话有了刻骨铭心的体会。

排查复盘时我意识到,问题不只在"忘记挂载",更在于我对容器可写层的认知太模糊。凡是要保留的数据,从一开始就要放到数据卷里,启动容器时用-v参数指定挂载关系:

docker run -d -p 5432:5432 -v pg-data:/var/lib/postgresql/data some-database-image

这里的pg-data就是命名数据卷,由Docker统一管理。也可以使用绑定挂载,把宿主机的绝对路径映射进容器。无论哪种方式,核心原则是一致的:容器本身不承担持久化职责,它是随时可以销毁重建的"消耗品"。

4.3 端口冲突:从报错到自定义网络的认知升级

第二个容器启动时,报错信息是port is already allocated。原因一眼就能看懂:宿主机上的某个端口已经被前一个容器占了。解决办法也不难,换一个没用过的端口,或者停掉占用端口的容器。但这个坑背后引出的知识点,比"换端口"重要得多——端口映射只是容器对外提供服务的方式之一,容器与容器之间的通信,根本不需要走宿主机端口,而是应该在同一个自定义网络里直接通信。

后来我用Compose编排一个带数据库的应用时,就让Web服务通过服务名访问数据库,比如http://database:3306,完全不需要关心数据库容器实际分配到哪个IP。这个机制依赖Docker自定义网络的DNS解析能力,弄懂之后,我对容器网络的认知从"端口转发"升级到了"服务发现",看问题的层次完全不同了。

4.4 构建缓存失效:Dockerfile指令顺序的现场教学

有段时间我经常改代码后重新构建镜像,发现docker build每次都从头执行所有步骤,明明只改了一行代码,却等了很久。第一次遇到时我很困惑,后来逐步排查,把焦点锁定在Dockerfile的指令顺序上。

Docker执行构建时,对于每条指令都会检查是否能复用已有缓存层。如果COPY的源文件内容有任何变化,即使后续的RUN指令本身没变,从COPY开始之后的所有层都会缓存失效。换句话说,把"经常变动的文件拷贝"放在"消耗时间长的依赖安装"前面,会让缓存机制形同虚设。

解决办法是把Dockerfile按"变动频率从低到高"重新组织。典型的改进顺序是:先COPY依赖清单文件,再执行依赖安装,最后COPY整个项目源码。这样只要依赖清单不变,依赖安装那一步就能命中缓存,构建速度快很多。

5. 从"会用命令"到"会设计镜像":进阶的三个关键点

5.1 分层缓存背后的设计哲学

前面讲了缓存失效的踩坑经历,反过来也说明一个道理:Dockerfile的指令顺序不仅影响可读性,更直接决定构建效率和镜像质量。学习结束后我重写了一个旧项目的Dockerfile,把原本六条指令压缩成五条,镜像体积从1.2G降到320M,构建时间从十几分钟缩短到三分钟左右。核心改动其实只有两个:一是合并多个RUN指令,减少镜像层数;二是把依赖安装挪到源码拷贝之前,让缓存命中率大幅提升。

这个改动的理论依据就是镜像分层机制。每一层只记录与上一层的差异,层数越多,镜像自然越大;层间缓存一旦被底层的变化破坏,其后的所有重建成本都会叠加。所以设计Dockerfile时,我会习惯性问自己四个问题:这个指令会新增一层吗?它依赖的文件多久变一次?能不能把不变的部分放到前面?这个基础镜像是不是最小可选方案?

5.2 多阶段构建:把镜像体积控制变成肌肉记忆

多阶段构建是课程里让我最惊喜的章节。它的思路很像"工地上分两批人干活":第一批用重型工具把材料加工成半成品,第二批只拿半成品进干净的房间做最终装配,所有使用过的重型工具和工具包装都留在工地外。映射到Docker,就是在同一个Dockerfile中写多个FROM阶段,第一个阶段跑编译、装依赖、生成产物,第二个阶段只拷贝第一阶段产物进去,用一个精简运行时镜像跑最终应用。

实际操作体验非常好。之前有一个需要编译的服务,如果只用一个基础镜像装完编译器再跑构建,最终镜像接近1G;改成多阶段构建后,运行镜像只需要一个运行时的精简基础镜像加一个编译产物,体积骤降到几十M。镜像变小,推送时间、拉取时间、磁盘占用、启动时间全部受益。对于任何带有编译步骤的项目,这是非常值得养成的设计习惯。

5.3 用Compose把整个应用栈写成一段描述

当项目从单容器扩展到"Web服务+数据库+缓存"多服务之后,手工敲docker run命令会越来越不可维护。docker-compose的价值就在于,把整个应用栈的启动方式写进一个YAML文件,然后一行命令拉起所有服务,一行命令全部清理。

一个典型的docker-compose.yml骨架大概长这样:

services: web: build: . ports: - "8000:8000" depends_on: - db db: image: postgres:16 environment: POSTGRES_PASSWORD: password volumes: - db-data:/var/lib/postgresql/data volumes: db-data:

注意depends_on只是控制服务的启动顺序,它并不能保证数据库已经完全就绪。Web服务启动时数据库可能还在初始化,这就会导致偶发的连接失败。严谨的做法是给数据库服务加上healthcheck健康检查,或者让应用层实现启动重试机制。这个细节在课程作业里不明显,但真实上线时它是一个经典隐患。

6. 学习结束后的沉淀:练习路径与心里话

6.1 如果让我重新排一条练习路径

课程结束后,我经常被问到一个问题:接下来怎么练?如果让我给后来者规划一份路线,我会建议按这个顺序走:第一步,用Docker跑一个自己真正熟悉的小项目,别用hello-world,用熟悉的代码会产生直接的正反馈;第二步,把以前经常出环境问题的旧项目用Docker重新跑起来,感受一次"环境复制"的快乐;第三步,给一个带依赖的小项目写Dockerfile,做体积优化,观察哪条指令影响了镜像大小;第四步,用Compose编排一个前后端加数据库的完整应用;最后,再去看Docker网络、存储、日志相关的原理文档。

这个路径的核心原则是"每次只引入一个不变量"。不要一上来就折腾完整的微服务集群,先从单容器开始,把每条命令背后的行为都弄清楚,再逐步叠加复杂度。每一步都要动过手、出过错、然后又自己解决掉,才算真正掌握。

6.2 给"半懂"状态的开发者几句实在话

我特别想跟那些跟我当初状态差不多的人说几句。第一,Docker的知识点很散,但学习曲线其实不算陡,一定要靠动手把知识点串成网;第二,别怕把环境搞坏,你用的是容器,弄坏了直接删掉重建,成本极低,这种"试错自由"本身就是Docker最大的好处之一;第三,遇到报错先划分阶段——是拉取阶段、构建阶段、启动阶段,还是容器运行阶段?把问题定位到阶段,排查范围立刻缩小大半。

就个人体验而言,这趟学习旅行让我收获最大的,不是背会了多少命令,而是面对环境问题时的心态变了。以前遇到环境问题,我的第一反应是"又要折腾了";现在的第一反应是"写个Dockerfile,把这套环境固化下来"。从害怕环境差异,到享受环境可复现带来的安全感,这种转变确实是只有亲手做完一遍才会发生的。如果你也想体验到这一步,就别只看资料,哪怕先拿本地一个最简单的脚本容器化练手,你都会发现,收获比想象多得多。

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

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

立即咨询