ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Docker容器入门指南:从镜像、容器到数据卷与网络的核心概念解析

Docker容器入门指南:从镜像、容器到数据卷与网络的核心概念解析

1. 为什么我劝你先搞懂容器到底在解决什么问题

很多人第一次接触Docker,脑子里冒出来的第一个念头是“这不就是个轻量级虚拟机吗”。我当初也这么想,结果在头一个月里踩了一堆坑,才慢慢把思路掰过来。如果你也刚准备入门,我建议先别急着敲命令,花十分钟把“它到底在解决什么”这件事想明白,后面学起来会顺很多。

先说一个我亲身经历的场景。早些年我在一个团队里做后端开发,本地跑得好好的服务,一放到测试环境就出问题。排查半天,发现是测试机上的某个依赖库版本和本地不一致。这种“在我机器上明明是好的”问题,几乎每个开发者都遇到过。传统做法是写一份长长的部署文档,把每一步操作、每一个依赖版本都记下来,但文档会过期,人手操作会出错,环境之间的差异永远填不平。

容器要解决的核心问题就是这个:把应用和它运行所需要的一切——代码、运行时、系统工具、库、配置——打包成一个标准化的单元。这个单元在任何支持容器的机器上跑起来,行为都是一致的。你可以把它想象成一个“自带厨房的餐车”,不管开到哪个城市,锅碗瓢盆、调料食材全都带着,做出来的味道不会变。

这里有个关键点容易被忽略:容器不是虚拟机。虚拟机是把整台硬件虚拟化,里面跑一个完整的操作系统,笨重、启动慢、占资源。容器是共享宿主机内核的,它隔离的是进程、网络、文件系统这些层面。所以一个容器镜像可能只有几十兆,启动只要一两秒。我第一次看到docker run秒起一个服务的时候,确实有点震撼。

那Docker在这个体系里扮演什么角色?简单说,Docker是目前最主流的一套容器化工具链,它把镜像构建、容器运行、网络管理、存储管理这些操作封装成了相对友好的命令和配置文件。没有Docker之前,容器技术已经存在,但用起来门槛很高。Docker的出现,让普通开发者也能几分钟内跑起来一个容器。

适合谁来学?我的判断是:只要你在做软件开发、测试、运维,或者对部署这件事有需求,都值得花时间。前端同学可以用它统一Node版本,后端同学可以用它搭本地依赖服务,测试同学可以用它快速拉起一套环境。哪怕你只是想在本地跑一个数据库又不想污染系统,容器也是极好的选择。

提示:入门阶段不要一上来就追求“精通”,先把“拉镜像、跑容器、看日志、进容器”这四个动作练熟,覆盖日常八成场景。

2. 装好之后先别急着敲命令,把这三个概念刻进脑子里

装Docker本身没什么好说的,各平台的安装包都很成熟,跟着官方指引走就行。真正值得花时间的是理解三个核心概念:镜像、容器、仓库。这三个词会贯穿你之后所有的操作,如果一开始就混淆,后面会越学越乱。

2.1 镜像:一份只读的“模板”

镜像可以理解成一个只读的模板或者“菜谱”。它里面包含了文件系统的快照和一堆元数据,比如启动时默认执行什么命令、暴露哪个端口、设置什么环境变量。镜像本身不会运行,它只是躺在那里的一份定义。

镜像有个很重要的特性叫分层存储。一个镜像通常由多个层叠加而成,每一层代表一次文件系统变更。比如你先基于一个基础系统镜像,装了个运行时,这是一层;再复制进去你的应用代码,又是一层。这种设计的好处是,多个镜像可以共享相同的底层,节省磁盘空间,构建时也能复用缓存。

我刚开始学的时候,总觉得镜像很神秘,后来发现用docker history看一眼它的构建历史,就大概明白它是怎么一层层堆出来的了。这个命令对排查“为什么镜像这么大”特别有用。

2.2 容器:镜像跑起来之后的“实例”

容器是镜像的运行实例。你可以把镜像和容器的关系类比成“类和对象”——镜像是类,容器是new出来的对象。一个镜像可以跑出无数个容器,它们之间互相隔离。

容器最上面会加一个可写层。所有在容器里做的修改,比如新建文件、改配置,都写在这一层。容器删掉,这一层也就没了。这就是为什么很多人第一次用容器会惊讶地发现“我改的东西怎么重启就没了”——因为你改的是可写层,而容器重建时用的是镜像的原始状态。

如果你确实需要持久化数据,得用数据卷或者绑定挂载,把数据存到容器外面。这个后面会细说。

2.3 仓库:镜像的“应用商店”

仓库是存放和分发镜像的地方。最常用的是公共仓库,里面有大量官方和社区维护的镜像,比如各种语言运行时、数据库、中间件。你docker pull的时候,就是从仓库把镜像拉到本地。

仓库里的镜像用“仓库名:标签”来标识,比如nginx:latest、python:3.11-slim。标签相当于版本号,不写标签默认是latest。这里有个坑我要提醒你:生产环境千万不要用latest,因为它会变,今天拉的和明天拉的可能是两个东西。一定要用明确的版本标签,保证可复现。

理解了这三个概念,你再看Docker的命令,就会发现它们其实就是在操作这三样东西:pull从仓库拿镜像,run把镜像变成容器,commit把容器存成镜像,push把镜像推回仓库。

3. 从零跑起第一个容器:一条命令背后的完整逻辑

概念清楚了,咱们动手。第一个容器我建议从最经典的开始,它能让你直观感受到“拉取、运行、访问、清理”这个完整闭环。

3.1 一条命令拆开看

假设你想跑一个Web服务,命令大概长这样:

docker run -d -p 8080:80 --name my-web nginx:1.25

别小看这一行,每个参数都有讲究。我逐个拆给你看:

  • docker run:创建并启动一个容器。这个动作其实包含了“如果本地没有镜像就先拉取”的步骤。
  • -d:后台运行。不加这个,容器会占着你的终端,日志直接刷屏。
  • -p 8080:80:端口映射,格式是“宿主机端口:容器端口”。意思是把宿主机的8080端口转发到容器的80端口。这里最容易搞反,记住前面是外面的,后面是里面的。
  • --name my-web:给容器起个名字。不起名字Docker会随机分配一个,后面你想操作它就得先查ID,很麻烦。
  • nginx:1.25:指定镜像和标签。

执行完之后,打开浏览器访问宿主机的8080端口,应该就能看到页面了。这一刻你其实已经完成了一次完整的容器化部署。

3.2 容器生命周期管理:别只会run

跑起来只是开始,日常用得更多的是查看和控制。我整理了一张表,把最常用的几个命令和它们的实际用途列出来:

命令作用我什么时候用它
docker ps查看运行中的容器确认服务是否还活着
docker ps -a查看所有容器,包括已停止的排查“刚才那个容器去哪了”
docker logs 容器名查看容器日志服务起不来时第一件事
docker exec -it 容器名 bash进入容器内部想看看里面文件长啥样
docker stop 容器名优雅停止容器正常关闭服务
docker rm 容器名删除容器清理不再需要的实例
docker rmi 镜像名删除镜像释放磁盘空间

这里有个新手常踩的坑:docker stop之后容器还在,只是停止了,要彻底清理得用docker rm。而且删除运行中的容器会报错,必须先停再删。我一开始总是忘记,后来养成了习惯,清理时直接docker rm -f强制删除,但生产环境慎用这个。

3.3 进容器这件事,比你想象的更有用

docker exec -it 容器名 bash这个命令我几乎每天都会用。它让你像登录一台远程机器一样进入容器内部。进去之后你可以查看文件、测试网络连通性、临时改配置调试。

但要注意,在容器里做的修改是临时的,容器一重建就没了。所以它适合调试,不适合当作正式的修改手段。正式修改应该改镜像的构建文件,重新构建镜像。

还有个细节:不是所有镜像里都有bash。有些精简镜像只有sh,这时候你得把命令换成docker exec -it 容器名 sh。如果连sh都没有,那这个镜像可能压根没打算让你进去,得换思路排查。

注意:进入容器后你拥有的是容器内用户的权限,不一定是root。如果遇到权限问题,可以加-u root参数,但生产环境要谨慎。

4. 镜像从哪来:拉取、查找与版本选择的门道

会用run之后,下一个问题自然是:镜像从哪来?怎么找到我想要的?怎么选版本?这块看似简单,其实有不少门道。

4.1 搜索和拉取的正确姿势

找镜像最直接的方式是docker search 关键词,但说实话,这个命令返回的结果排序和展示信息比较有限,我更多是直接去公共仓库的网页端看。网页端能看到镜像的详细说明、标签列表、更新时间和下载量,信息全得多。

拉取用docker pull 镜像名:标签。如果不指定标签,默认拉latest。前面说过,生产环境别用latest,但本地学习阶段用用也无妨,图个方便。

拉取的时候你会看到一堆进度条,每一行对应镜像的一个层。如果某个层本地已经有了,它会显示“Already exists”,直接复用,不会重复下载。这就是分层存储带来的好处。

4.2 标签怎么选:一个容易被忽视的决策

同一个镜像往往有几十个标签,怎么选是个技术活。我总结了几条经验:

  • 优先选明确版本号,比如1.25.3而不是1.25,更不是latest。版本越具体,行为越可预测。
  • 注意基础系统的差异。很多镜像会提供基于不同基础系统的变体,比如基于完整发行版的、基于精简发行版的、基于更小体积发行版的。体积小的启动快、攻击面小,但可能缺一些常用工具,调试起来不方便。我一般开发阶段用完整版,生产用精简版。
  • 留意架构。如果你用的是特殊架构的机器,要确认镜像是否支持。现在主流镜像基本都支持常见架构,但一些小众镜像可能只有单一架构。

选标签这件事没有绝对标准,核心原则是:让构建和部署可复现。你今天跑起来的东西,三个月后换台机器还能跑出一模一样的结果,这才算合格。

4.3 镜像的清理:磁盘空间是怎么被吃掉的

用不了多久你就会发现磁盘空间告急。原因很简单:每次构建、每次拉取,都会留下镜像层;停止的容器、悬空的镜像、没用的数据卷,都在悄悄占地方。

我常用的清理命令是docker system prune,它会清理停止的容器、悬空镜像、未使用的网络。加-a参数会连未被使用的镜像一起清掉,加--volumes会连数据卷一起清。但这个命令要慎用,它会删掉你暂时没在用但可能还需要的东西。

更稳妥的做法是先docker system df看一眼空间占用情况,心里有数了再决定清什么。这个命令会列出镜像、容器、数据卷、构建缓存各自占了多少,非常直观。

5. 让数据活下来:数据卷与挂载的实战选择

容器一删数据就没,这是新手最容易崩溃的点。解决办法就是把数据放到容器外面。Docker提供了两种主要方式:数据卷和绑定挂载。它们看起来都能实现“数据持久化”,但适用场景差别很大。

5.1 数据卷:让Docker帮你管

数据卷是Docker管理的存储区域,存在宿主机的特定目录下,但具体路径由Docker维护,你不用关心。创建和使用都很简单:

docker volume create my-data docker run -d -v my-data:/app/data --name my-app my-image

-v my-data:/app/data的意思是把名为my-data的数据卷挂载到容器的/app/data目录。之后容器里往这个目录写的东西,都会落到数据卷里。容器删了,数据卷还在,下次新容器挂上同一个卷,数据就回来了。

数据卷的好处是跨平台一致、由Docker管理、适合生产环境。缺点是你在宿主机上直接找这些文件不太方便,得通过docker volume inspect查路径。

5.2 绑定挂载:直接映射宿主机目录

绑定挂载是把宿主机的某个具体目录直接映射进容器:

docker run -d -v /home/user/config:/app/config --name my-app my-image

这样宿主机/home/user/config里的东西,在容器里/app/config就能看到,改一边另一边立刻生效。开发阶段我特别喜欢用这个,代码改完不用重新构建镜像,刷新一下就生效。

但绑定挂载有个坑:宿主机目录的权限和容器内用户的权限可能对不上。我遇到过容器里进程没有权限写挂载目录的情况,排查半天才发现是用户ID不匹配。解决办法要么调整宿主机目录权限,要么在容器里用对应的用户运行。

5.3 怎么选:一张表说清楚

维度数据卷绑定挂载
管理方Docker你自己
跨平台好依赖宿主机路径
适合场景生产数据持久化开发时挂代码、挂配置
权限问题较少常见
可移植性高低

我的习惯是:开发用绑定挂载图方便,生产用数据卷图省心。两者也可以混用,比如代码用绑定挂载,数据库数据用数据卷。

提示:不管用哪种方式,都要记得备份。数据卷虽然持久,但也不是不会丢。定期把重要数据导出到别处,这个习惯能救命。

6. 网络这件事:容器之间怎么互相找到对方

单跑一个容器没什么网络问题,但真实项目往往是多个容器协作:应用容器要连数据库容器,前端容器要调后端容器。这时候网络配置就绕不开了。

6.1 默认网络:能通,但不好用

Docker安装后会自带几个默认网络,容器默认连的是其中一个桥接网络。在这个网络里,容器之间可以通过IP互相访问,但IP是会变的,容器重建一次IP可能就变了。靠IP通信显然不靠谱。

而且默认桥接网络里,容器之间只能用IP,不能用名字互相解析。这就很尴尬了,你总不能每次重建容器都去改配置里的IP吧。

6.2 自定义网络:让容器用名字互相找

正确做法是创建一个自定义网络:

docker network create my-net docker run -d --network my-net --name db my-db-image docker run -d --network my-net --name app my-app-image

在同一个自定义网络里,容器可以直接用容器名作为主机名互相访问。也就是说,app容器里连数据库,主机名直接写db就行,不用管IP是多少。这个特性叫服务发现,是自定义网络相比默认网络最大的优势。

我强烈建议:只要涉及多容器协作,就建自定义网络。这一步多花十秒钟,后面省掉无数排查IP的麻烦。

6.3 端口暴露的边界:哪些该暴露,哪些不该

端口映射-p是把容器端口暴露到宿主机。但并不是所有容器都需要暴露端口。比如数据库容器,如果只有应用容器需要连它,那完全不需要-p,让它们在同一网络里通过容器名通信就行。暴露到宿主机反而增加了安全风险。

我的原则是:只有需要从宿主机或外部访问的服务才映射端口。内部服务之间的通信,走容器网络就够了。这样既安全又清晰。

7. 构建自己的镜像:从能跑到跑得好的距离

用别人的镜像总有不够用的时候,迟早要构建自己的。构建镜像的核心是写一个构建文件,里面描述“从哪个基础镜像开始,做哪些操作,最后怎么启动”。

7.1 构建文件的基本结构

一个典型的构建文件大概长这样:

FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["python", "app.py"]

逐行看:FROM指定基础镜像;WORKDIR设置工作目录;COPY把文件复制进去;RUN执行构建时的命令;EXPOSE声明容器监听的端口(只是声明,实际映射还是靠-p);CMD指定容器启动时执行的命令。

7.2 层缓存:构建快慢的关键

构建文件里指令的顺序直接影响构建速度,因为每一条指令都会生成一层,某一层没变就能复用缓存。

看上面那个例子,为什么先COPY requirements.txt再RUN pip install,最后才COPY . .?因为依赖文件很少变,代码经常变。这样改代码时,依赖安装那一层还能用缓存,只有复制代码那一层重新执行。如果反过来先复制全部代码再装依赖,那每次改代码都会导致依赖重装,构建时间翻好几倍。

这个技巧我用了很多年,是优化构建速度最有效的一招。判断标准很简单:把变化频率低的操作放前面,变化频率高的放后面。

7.3 镜像瘦身:能小为什么要大

镜像越小,拉取越快、启动越快、占空间越少。几个实用的瘦身手段:

  • 选精简基础镜像。很多官方镜像都提供了精简变体,体积可能只有完整版的几分之一。
  • 合并RUN指令。多个RUN会生成多层,合并成一条能减少层数。记得在一条RUN里清理掉临时文件。
  • 用多阶段构建。编译阶段用完整镜像,运行阶段只复制编译产物到精简镜像。这样最终镜像里没有编译工具链,体积能小很多。
  • 写忽略文件。把不需要复制进镜像的文件(比如版本控制目录、本地配置、日志)排除掉,既减小体积又避免泄露敏感信息。

我见过一个项目,镜像从一点几个G优化到一百多兆,启动时间从十几秒降到两秒。瘦身这件事,投入产出比很高。

8. 编排入门:当容器从一个变成一群

单容器用docker run就够了,但当你有五六个容器要一起管理,手动敲命令就顶不住了。你需要一个工具来描述“我要哪些容器、它们怎么连、怎么启动”,然后一条命令全部拉起。

8.1 编排文件长什么样

编排工具用一份声明式文件描述整个应用。大概结构是这样:

services: web: image: my-web:1.0 ports: - "8080:80" depends_on: - db db: image: my-db:1.0 volumes: - db-data:/var/lib/data volumes: db-data:

这份文件描述了两个服务:web和db。web映射了端口,依赖db;db挂了一个数据卷。启动时一条命令,编排工具会按依赖顺序把容器都拉起来,网络也自动配好。

8.2 编排解决了哪些手动做不到的事

手动docker run有几个绕不开的痛点,编排工具正好补上:

  • 依赖顺序。手动启动你得自己记住先起数据库再起应用,编排文件里声明依赖后自动处理。
  • 统一网络。编排工具会自动创建一个网络,所有服务都在里面,用服务名互相访问。
  • 一键启停。一条命令启动全部,一条命令停止全部,不用一个个敲。
  • 配置集中。所有服务的配置在一份文件里,一目了然,也方便版本管理。

我现在的习惯是:只要超过两个容器,就用编排文件。哪怕本地开发也一样,因为写一次之后,换台机器一条命令就能复现整个环境。

8.3 从单容器到编排的过渡建议

别一上来就学编排,容易懵。我的建议路径是:先用docker run把单个容器玩熟,理解镜像、容器、网络、数据卷;然后手动用docker run拉起两个容器,让它们通过网络互相通信;等你觉得手动敲命令烦了,再上编排工具。这时候你会发现编排文件里的每个配置项你都能对应到之前手动敲过的命令,理解起来毫无障碍。

9. 我踩过的那些坑:新手最容易翻车的地方

学Docker的过程中我踩了不少坑,有些当时排查了很久。这里挑几个最有代表性的分享出来,希望能帮你省点时间。

9.1 容器一停数据就没了

这是最经典的坑。很多人第一次用数据库容器,跑起来建了表、插了数据,一重启发现全没了。原因就是没挂数据卷,数据写在了容器的可写层,容器一重建就清空。

解决办法前面说过,挂数据卷或者绑定挂载。养成习惯:只要容器里会产生需要保留的数据,第一件事就是挂载。

9.2 端口映射写反了

-p 8080:80和-p 80:8080是完全不同的意思。前者是宿主机8080转到容器80,后者反过来。我见过有人排查半天“为什么访问不了”,最后发现是端口写反了。

记住口诀:前面是门牌号(宿主机),后面是房间号(容器)。访问的时候用门牌号。

9.3 镜像标签用了latest导致行为不一致

本地测试用latest没问题,部署到另一台机器上拉了同一个latest,结果行为不一样。因为latest指向的镜像可能已经更新了。这种问题排查起来特别费劲,因为你看不出哪里变了。

解决办法就是永远用明确版本标签。构建时打上版本号,部署时引用版本号,保证每次跑的都是同一个东西。

9.4 容器里改了配置,重建后失效

在容器里直接改配置文件,当时生效了,但容器一重建就回到原样。因为改的是可写层,重建时用的是镜像原始状态。

正确做法是改构建文件,重新构建镜像;或者把配置文件通过绑定挂载挂进去,改宿主机上的文件。前者适合正式变更,后者适合开发调试。

9.5 磁盘被悄悄占满

用了一段时间发现磁盘满了,一查全是停止的容器、悬空镜像、构建缓存。这些东西不会自动清理,得手动处理。

我的习惯是每隔一段时间跑一次docker system df看看占用,然后有针对性地清理。别等到磁盘满了才想起来,那时候可能连日志都写不进去了。

10. 入门之后往哪走:几条我验证过的进阶路线

把上面这些内容练熟,日常开发部署基本够用了。如果你想继续深入,我分享几条自己走过的路线,供你参考。

第一条是深入镜像构建。研究多阶段构建、构建参数、跨平台构建这些话题,把镜像做得又小又安全。这条路线对部署效率提升最明显。

第二条是深入网络和存储。搞清楚不同网络模式的差异、数据卷的底层机制、各种挂载方式的性能特点。这条路线能让你在复杂场景下做出更合理的架构决策。

第三条是深入编排和集群。从单机编排走向多机集群,理解服务发现、负载均衡、滚动更新、健康检查这些概念。这条路线适合往运维和架构方向发展的同学。

第四条是深入安全。研究镜像扫描、最小权限、只读文件系统、资源限制这些话题。容器安全是个容易被忽视但非常重要的领域。

我的建议是别贪多,选一条你最需要的先走深。我自己是先走构建优化,再走编排,最后补的安全。每条路线走下来,回头看之前的操作都会有新的理解。

最后分享一个我坚持了很久的习惯:每学一个新命令或新概念,就动手跑一遍,然后用自己的话记下来。Docker这东西,看十遍不如跑一遍。我当初就是靠着一个又一个“跑起来、搞坏、修好”的循环,慢慢把这块知识变成了自己的东西。

RELATED READING

延伸阅读

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