news 2026/10/19 5:52:56

Docker核心概念解析:镜像、容器、数据卷与Dockerfile实操指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker核心概念解析:镜像、容器、数据卷与Dockerfile实操指南

很多新手把第一篇看完,装好了环境,敲出了第一个 hello-world,结果到了真正要用 Docker 的时候反而懵住了:镜像、容器、仓库到底是什么关系?为什么同一个命令别人能跑起来,自己一运行容器就秒退?为什么容器删了之后,之前在里头改的东西全没了?

这些问题卡住的不是少数人。网上讲 Docker 的教程非常多,但大部分要么直接甩命令,要么一上来就贴一堆底层原理,中间缺了最关键的一层——概念和实操之间的桥。这篇作为教程的第二篇,我不打算再重复安装和 hello-world 那点事,而是把镜像、容器、仓库、数据卷、Dockerfile 这些绕不开的核心概念,配合一组一组真实场景下的命令讲清楚。适合刚看完第一篇、还没正式跑起自己服务的朋友,或者是被那句"容器就是轻量级虚拟机"误导过的人。

先说明一点:这一篇讲的东西很基础,但基础不等于简单。把这几块弄明白,后面无论是用 Docker 部署 Web 服务、跑数据库,还是自己写 Dockerfile 做镜像,都不会再觉得别扭。

1. 镜像、容器、仓库:三个最容易搞混的角色,一次性讲清楚

1.1 生活化类比:镜像是一张"出生光盘",容器是一台"正在运行的电脑"

镜像和容器这两个词,是 Docker 新手第一道坎。很多人以为它俩是一回事,其实区别非常直观。

想象一下你买了一张游戏光盘。光盘本身是只读的,里面刻着完整的游戏数据,这张光盘就是镜像。你把光盘塞进游戏机,游戏跑起来了,你可以新建存档、调整设置、打补丁,这是容器。游戏机可以同时插两张同款光盘,两个存档互不干扰,这就是一个镜像可以运行出多个容器。

更贴切一点说,镜像是一个模板,它包含了程序运行所需的完整文件系统、依赖库、配置文件和启动命令。容器是基于这个模板创建出来的运行实例,它有自己独立的可写空间。镜像本身一旦构建完成就不该再改动,容器则随时可以被创建、启动、停止、删除。

很多老教程喜欢说"容器是轻量级虚拟机",这个类比方便归方便,但会误导人。虚拟机里跑的是完整的客户机操作系统,有自己独立的内核;容器和宿主机共享内核,只是通过命名空间做了隔离,所以启动才能做到秒级,资源占用才那么低。你把它理解成"同一台电脑上开了几个互相看不见的沙箱"会更准确。

1.2 分层存储:为什么镜像仓库看似很大,但拉取往往很快

镜像还有一个让新手困惑的特性:分层。

镜像不是一块完整的大硬盘,而是由一层一层的只读文件系统叠加组成的。每一层只记录相对于上一层的改动。拉镜像的时候,Docker 会逐层下载,如果本机已经有了某个底层,这层就直接复用,不再重复下载。

这个设计带来的直接体验就是:你第一次拉一个 ubuntu 基础镜像可能要下几百兆,但之后再拉基于同一个底层的其他镜像,比如 nginx、mysql 的官方镜像,速度会快很多,因为基础层已经在了,实际只需要下载各自的上层差异。很多人在这一步误以为"这个镜像真小",其实不是小,是分层复用的功劳。

分层还有一个好处是磁盘占用。十个镜像如果都基于同一个 Debian 基础层,底层文件在磁盘上只有一份,大家共享。这也是为什么 Docker 用久了,docker system df里看到的镜像体积总和,通常比实际占用小不少。

1.3 可写层与容器的"临时性":容器不是虚拟机,别在里面慢慢装

镜像本身只读,那容器运行时产生的文件写到哪去?

Docker 会给每个容器分配一个可写层。容器里所有文件改动,都记录在这个可写层里。这层是容器私有的,容器一删除,可写层也跟着销毁,你之前在容器里安装的软件、改的配置、落盘的临时数据,全部连锅端。

这就是容器和虚拟机最本质的一个区别:虚拟机里装的软件,关机重启后还在,磁盘是虚拟硬盘,数据持久化。容器不是这样,容器天然是"临时的"。正确用法应该是,需要持久化的数据放到数据卷里(后面会细讲),需要固化的环境写进镜像里,而不是把容器当作一台小虚拟机,启动后慢慢往里面装环境,装完觉得万事大吉。

把这个心智模型建立起来,后面所有操作都不会再觉得别扭。镜像负责"环境长什么样",容器负责"环境跑起来",仓库负责"镜像从哪来"。

2. 新手必背的一组命令:从拉取到跑起来的完整操作链

2.1 跑起第一个服务:docker run 的常用参数拆解

概念说再多,不如实际操作一遍。我拿一个最简单的场景举例:在本地启动一个 nginx 服务,访问它看到一个网页。

第一步,从公共镜像仓库把镜像拉到本地:

docker pull nginx:1.25-alpine

这里我刻意指定了标签1.25-alpine。alpine是一个体积很小的基础系统,nginx 官方基于它做的镜像只有几十兆,比默认的nginx:latest小很多。新手第一次拉镜像建议直接选 alpine 标签,速度快、后面构建自定义镜像也方便。

然后启动一个容器:

docker run --name myweb -d -p 8080:80 nginx:1.25-alpine

跑完之后浏览器访问http://localhost:8080,如果看到 nginx 的欢迎页,这条命令就算彻底跑通了。

我们来拆解一下参数,这几个参数你以后会天天用:

  • --name myweb:给容器起个名字,后面docker stop myweb直接引用名字,比记一串随机 ID 省心太多。
  • -d:后台运行。不加这个参数的话,容器会在前台运行,终端直接挂住,日志不断刷屏,你只能 Ctrl+C 退出,退出一瞬间容器也就停了。Web 服务、数据库这类长驻进程,基本都用-d。
  • -p 8080:80:端口映射,宿主机 8080 端口转发到容器 80 端口。容器本身有自己独立的网络命名空间,不映射的话,宿主机访问不到容器里的服务。

对了,-p左侧是宿主机端口,右侧是容器端口。我见过不止一次有人把两边写反,然后到处问为什么访问不了。

2.2 生命周期管理:从 docker ps 到 docker rm 的心智模型

容器跑起来之后,会有几个非常高频的场景。

查看正在运行的容器:

docker ps

注意,docker ps默认只显示运行中的容器。一个容器启动时如果是因为某种原因退出了,它不会出现在这个列表里,而是出现在:

docker ps -a

-a表示 all,所有的容器,包括正在跑的和已经退出的。很多新手容器"跑丢"了,急得不行,其实多半是docker ps看不到它就以为没了,其实用docker ps -a一眼就能找到。

停止、启动、重启:

docker stop myweb docker start myweb docker restart myweb

删除容器:

docker rm myweb

如果容器还在运行,直接删会报错,需要先docker stop再删,或者用docker rm -f myweb强制删。容器彻底不需要了,就删掉,别留着堆内存和磁盘。

清理所有已经退出的容器,可以一条命令搞定:

docker container prune

这个命令会删除所有处于 exited 状态的容器,执行前它会问你要不要继续,不用慌。整体心智模型就一句话:stop是关机,rm是丢掉这台机器,删容器不等于删镜像,镜像还在本地docker images里躺着。

2.3 调试两件宝:docker logs 和 docker exec

容器是隔离的,启动之后你没法像看虚拟机界面那样直接看里面发生了什么,这时候两个命令是救命稻草。

查看容器日志:

docker logs -f myweb

-f是 follow,跟踪输出,类似tail -f。容器启动失败、服务报错、访问日志,全都可以从这里看到。排查问题时第一件事永远是docker logs,别瞎猜。

进入容器内部执行命令:

docker exec -it myweb bash

-it其实是两个参数,-i保持标准输入打开,-t分配一个伪终端,组合起来的效果就是你可以像 ssh 到一台机器上一样,在容器里敲命令。nginx 镜像里自带 bash,可以直接进;有些极简镜像不带 bash 只有 sh,就把命令换成sh。

注意,进入容器只是调试用的,不是让你进去装东西的。我在 1.3 节强调过,容器是临时的,你此刻在容器里装的任何东西,容器一删全没。把docker exec当作查看现场的手段,而不是运维入口。

为了方便记忆,我把这组命令整理成一个快查表:

命令作用备注
docker pull 镜像名拉取镜像建议带版本标签
docker images查看本地镜像构建镜像的原材料
docker run -d --name xxx -p 端口:端口 镜像启动容器参数按需增减
docker ps / docker ps -a查看运行中/所有容器排查先看 -a
docker logs -f 容器名跟踪日志排错第一抓手
docker exec -it 容器名 bash进入容器临时调试用
docker stop / start / restart 容器名生命周期控制类似关机和开机
docker rm 容器名删除容器-f 强制删除

3. 环境固化的核心:写一份自己的 Dockerfile

3.1 第一个 Dockerfile:目标要足够简单,才能看清原理

用现成的镜像解决不了所有问题。大部分真实场景是:代码是你自己的,依赖是你自己的,配置也是你的,你得把"自己的东西"和"别人的镜像基础"组合成一个新镜像。这时候就要写 Dockerfile。

Dockerfile 本质上是一份构建说明书,告诉 Docker 从哪开始、执行哪些命令、拷贝哪些文件、最终启动时跑什么。它最大的价值是"环境即代码":你同事拉走你的镜像,或者照着你的 Dockerfile 重新构建,得到的环境和你本地一模一样,再也不会有"我机器上明明好好的"这种问题。

我用一个最简示例来拆解。假设你在本地有一个index.html,内容随意,你想做一个镜像,把 nginx 跑起来并展示这个页面。Dockerfile 这样写:

FROM nginx:1.25-alpine COPY index.html /usr/share/nginx/html/index.html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]

每一行都讲清楚:

  • FROM:指定基础镜像,这是构建的起点。这里的 nginx 镜像已经把 nginx 装好了,文件结构也是现成的,你只需要做增量修改。
  • COPY:把当前目录下的 index.html 拷贝到镜像里的 nginx 网页根目录。这里有个前提:index.html和 Dockerfile 必须放在同一个目录下,这个目录叫构建上下文。
  • EXPOSE:声明容器对外提供服务的端口。注意它只是一个声明,真正生效的还是运行时-p映射,但写清楚能让人一眼看懂这个镜像开的是哪个口。
  • CMD:容器启动时执行的命令。nginx 默认的官方镜像其实已经内置了合适的 CMD,这里写出来是为了解释概念——容器启动后必须有一个前台进程占住,否则会被系统判定为"没事干了",直接结束进程退出容器。

在 Dockerfile 所在目录执行构建:

docker build -t myweb:v1 .

-t是给镜像打标签,格式是名字:版本。末尾那个点表示构建上下文为当前目录。构建完成后docker images就能看到myweb:v1,然后docker run --name myweb-test -d -p 8080:80 myweb:v1跑起来,浏览器访问看看页面是不是换成了你自己的 index.html。

3.2 一次 RUN 一个层:构建过程与体积控制的关键

写 Dockerfile 有个容易忽视的原理:每一条会改变镜像内容的指令(比如RUN、COPY)都会产生一个新的镜像层,最终镜像由这些层叠加而成。

这个特性带来的直接影响是体积膨胀。举个例子,如果你想在镜像里安装一些工具,不要一个工具写一条RUN:

RUN apt-get update RUN apt-get install -y curl vim

这样会留下三层,其中apt-get update产生的临时缓存也会被完整留在这一层里,白白占空间。更合理的写法是合并成一条:

RUN apt-get update && apt-get install -y curl vim

用&&串起来,构建时只产生一层,apt-get update的缓存也能在同一层里被清理掉,镜像体积小不少。

但是注意,合并命令别走火入魔。有些人为了追求层数少,把所有命令用一大堆&&串起来,一条指令几百个字符,构建失败后定位问题都费劲。我的建议是:相关命令合并,比如update和install必须合并,因为不合并会留下过期的索引缓存;无关命令分开写,保证可读性。层数和可读性的平衡,比单纯追求"层越少越好"更重要。

另外补一个很多新手不知道的细节:docker build会把整个构建上下文打包发给 Docker 引擎,如果目录里有大量无关文件,比如 node_modules、.git、临时文件,每次构建都会做一次巨量传输。这时候在目录里建一个.dockerignore文件,语法类似.gitignore,把不需要的文件排除掉:

node_modules .git *.log

这玩意能直接改变构建速度,项目越大体感越明显。

3.3 ENTRYPOINT 和 CMD:一张表理清启动命令

Dockerfile 里负责定义启动行为的指令有两个:ENTRYPOINT和CMD。新手经常搞混,我直接上结论。

  • CMD:提供默认启动命令或参数。Docker run 启动容器时,如果没有额外指定命令,就执行 CMD;如果指定了,CMD 会被覆盖。
  • ENTRYPOINT:容器启动时真正要执行的命令,它不会因为你在 docker run 后面追加参数而被覆盖,追加的参数会作为它后面的参数传进去。

两者配合最常见的方式是:ENTRYPOINT固定写主程序,CMD写默认参数。比如:

ENTRYPOINT ["nginx"] CMD ["-g", "daemon off;"]

这样 docker run 时如果你追加参数,本质上是替换掉默认参数,主程序永远不会变。

指令是否会被 docker run 追加命令覆盖典型用法
CMD会被覆盖提供默认参数
ENTRYPOINT不会被覆盖固定主程序/入口脚本
两者都用CMD 作为 ENTRYPOINT 的默认参数既要固定主程序又要可调参数

看完这张表再看CMD ["nginx", "-g", "daemon off;"]这个写法就很清晰了:方括号里是 JSON 数组格式,表示不加 shell 直接执行命令;daemon off;是让 nginx 前台运行的关键参数,因为容器里不能跑后台守护进程,必须有一个前台进程把容器"撑住",否则容器启动后立刻退出。

4. 容器删了数据还在:数据卷和端口映射的正确打开方式

4.1 容器删除后的一切归零:为什么需要数据卷

前面反复强调容器删除后数据会没,用一句话解释就是:数据写在了容器的可写层里。这一层随着容器销毁而销毁。

你可能会想:那我不用 Docker 存数据不就行了?问题是很多服务天然要存数据,比如数据库、文件存储、日志。举一个很现实的例子:你用 Docker 跑了一个 MySQL,往里面写了业务数据,某天你想升级 MySQL 镜像版本,停掉旧容器、删掉它、用新镜像再跑一个。你会发现数据全没了,数据库空空如也。谁遇到谁知道,那一刻真能急出一身汗。

解决思路不是"别删容器",而是把数据放到容器外部。Docker 专门为这个场景提供了数据卷(volume),卷的生命周期独立于容器,容器删了卷还在,数据就还在。

创建一个数据卷:

docker volume create mydata

运行容器时挂载它:

docker run -d --name mysql-test -v mydata:/var/lib/mysql mysql:8

这里的-v mydata:/var/lib/mysql,斜杠左边是卷名,右边是容器内的挂载路径。MySQL 会把数据文件写到/var/lib/mysql,而 Docr 在底层已经把它重定向到mydata这个卷里,容器删除再重建同一个命令,数据自动恢复。生产环境里数据库容器几乎都这么跑。

4.2 数据卷与挂载:宿主机与容器共享文件的正确姿势

数据卷之外还有一种更实际的方式:宿主机目录挂载。语法和卷类似,但左侧从卷名变成宿主机绝对路径:

docker run -d --name nginx-custom -p 8080:80 -v /home/you/website:/usr/share/nginx/html nginx

效果是宿主机/home/you/website目录和容器内网页根目录共享同一份文件,你改宿主机里的页面文件,容器立刻就能服务新内容,不用重新构建镜像。日志场景也一样,nginx 默认写日志的路径挂到宿主机,日志文件就落在本地磁盘上,后续接日志收集工具非常方便。

这个模式对开发调试尤其好用:代码在宿主机编辑,容器跑服务,两边实时同步,改完代码容器里立即可见,不用反复docker build。很多前面写的"本机开发改代码要重新打镜像"的焦虑,到这里自然就解开了。

还有一组经常用到的命令:

docker cp 宿主机路径 容器名:容器路径 # 文件拷进容器 docker cp 容器名:容器路径 宿主机路径 # 文件拷出容器

docker cp适合临时传文件,但它是一次性拷贝,不是同步。想保持同步,老老实实用-v挂载。

4.3 端口映射:-p 8080:80 里哪个是哪个,别再搞错

端口映射是新手最容易晕的地方。我再把这个规则用大白话讲一次。

容器有自己独立的网络命名空间,相当于一台没有独立 IP 对外可见的"内网主机"。想让外面访问它,必须做一个"门牌号"映射:宿主机开一个口,把流量转发到容器里的端口。

命令写法:

docker run -p 宿主机端口:容器端口 镜像名

所以-p 8080:80就是把宿主机 8080 端口收到的请求,全部转发给容器里的 80 端口。你访问localhost:8080,实际上是容器里的 nginx 在响应。容器内的服务如果想换端口,比如 nginx 在容器里监听了 8080,那就写-p 8080:8080或者-p 9090:8080,完全没有限制。

补充一句:-p后面可以不写宿主机端口,比如-p 80,Docker 会随机分配一个宿主机空闲端口,然后用docker port 容器名查看实际被分配了哪个端口。脚本化、自动化的场景偶尔用到,刚开始可以不深究。

关于网络模式,新手现阶段只需要知道默认的 bridge 模式足够用。还有一个 host 模式,容器直接共享宿主机的网络栈,端口就不用映射了,但会带来端口冲突的风险,不如 bridge 干净。

5. 真实翻车现场:破解容器秒退、时区错乱与镜像体积焦虑

5.1 容器总是"秒退",或者启动后立即结束

这个现象太经典了。启动容器命令敲下去,docker ps一看什么都没有,百思不得其解。

别急,先执行docker ps -a看所有容器。你会看到那个容器的状态栏写着Exited (0)或者类似的样子。后面括号里的数字是退出码,它已经在告诉你原因了。

最常见的原因是:容器里没有一个前台进程在运行。Docker 容器的生命周期绑定于它启动时启动的那个主进程,这个进程一旦结束,容器就认为"任务完成",立刻退出。

典型错误写法,比如有人用基础系统镜像个启动就跑一个后台服务:

docker run -d -it ubuntu service nginx start

service nginx start启动完会立刻返回,主进程结束,容器随之退出。这不是 Docker 的 Bug,而是它的设计逻辑:容器必须有一个长期运行的进程占住前台。真正的启动方式应该是让主进程以前台模式运行,比如nginx -g 'daemon off;',或者用tail -f /dev/null这种命令硬占住前台——后者只是调试临时用,别当正经方案。

退码的含义也可以记两个:Exited (0)表示主进程自己正常结束了,最可能就是后台命令秒退;Exited (127)多半是命令不存在,比如 CMD 里写了个根本没安装的程序。

5.2 时区、软件源和基础镜像选择:三个隐形坑

还有一个特别容易踩的坑:时区。官方镜像的基本时区基本都是 UTC,比我们日常时间慢 8 个小时。你在容器里跑日志任务,发现打印出来的时间怎么都对不上,查了半天最后发现是容器时区问题。

解决办法很简单,运行容器时传环境变量:

docker run -e TZ=Asia/Shanghai ...

或者在 Dockerfile 里提前设好。

还有一个坑是软件源。有些基础镜像自带的软件源可能因为各种原因不稳定,构建镜像时执行apt-get update慢得像蜗牛。处理思路是换成可用的镜像源再执行安装。但要注意:不同发行版的源写法完全不同,Debian/Ubuntu 系用的是 apt,Alpine 用的是 apk,文件路径和仓库格式都不一样,对着网上教程换源前一定先确认自己装的是哪一类系统。

基础镜像选择也是个隐形的坑。Alpine 系镜像小,层也少,但因为它阉割了一些东西,有些二进制程序跑起来会有兼容性问题,如果遇到编译类工具,缺依赖率更高。Debian/Ubuntu 系镜像体积大,但环境完整,省心很多。我的选择标准是:自己玩、对体积敏感的用 Alpine;跑正经服务、不想跟依赖纠缠的用 Debian slim 或 Ubuntu 的基础镜像。

5.3 镜像体积焦虑:瘦身有方法,但不是越瘦越好

网上关于"镜像瘦身"的文章特别多,动不动就是"从 1G 减到 50M"。看多了容易产生一种错觉:镜像体积越小就越牛。我实际用下来觉得这个目标需要理性看待。

体积小确实有好处:拉取快、部署快、磁盘省。但代价往往也很实在:构建过程复杂、调试不方便、某些官方镜像基于不同系统裁剪后缺这缺那。有些镜像为了小,把调试工具、curl、vim、ping 全删了,真出了故障进容器排查,发现连ps命令都没有,那种绝望谁试谁知道。

我的建议是:初次优化把目标放在"消除明显浪费"上。比如合并 RUN 命令清除缓存、用对体积友好的基础镜像、写.dockerignore防止把巨型目录打进去、用多阶段构建把最终镜像里的编译工具链卸掉。这些动作做下来,体积通常已经比原始版本小一大截,够用且干净。至于再往下压的那几十兆,除非是部署在频繁拉取大规模镜像的生产环境,否则投入产出比很一般。

镜像体积是"构建速度、可维护性、运行效率"综合权衡中的一个因子,而不是唯一的 KPI。追求合适,别追求极限。

说到这,还得再强调一下学习路线:镜像决定环境长什么样,容器让环境跑起来,Dockerfile 让环境可复制,数据卷让数据不随容器消亡。这四个点串起来,Docker 的主干就算通了。我平时给同事讲的时候,最常说的一句话是——把容器当成一次性的临时执行环境,该持久化的数据挂出来,该固化的环境写进镜像,别想着去“养”一个容器。带着这个思路去操作,踩坑次数能少一半。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 5:52:36

智能分类中的特征选择与模型训练:从原理到实战排查

刚接了一个活儿,给某企业内部做一套“智能分类”的快速验证项目,数据是几百个维度的表格,样本量只有几千条。最开始跑了一个最基础的逻辑回归,训练集准确率很好看,一上验证集就崩,查了半天才发现问题出在特…

作者头像 李华
网站建设 2026/10/19 5:51:32

极小函数依赖集求解算法:属性闭包与冗余消除四步通关

数据库复习到这个阶段,大部分人的心态是:函数依赖听得懂,属性闭包也算过两遍,一看“极小函数依赖集求解算法”这道大题的步骤,脑子里还是一团浆糊。期末卷子里这道题分值不低,而且它不只是考一个定义&#…

作者头像 李华
网站建设 2026/10/19 5:50:47

artcraft:从图片到手工图纸的图像处理与色号匹配实践

1. artcraft 到底在做什么,我为什么折腾这个项目先交代一下背景。我平时喜欢折腾各种小工具,也玩过一阵拼豆、十字绣和串珠。这两件事看起来八竿子打不着,但实际撞到一起之后,我发现自己反复遇到同一个问题:网上一堆好…

作者头像 李华
网站建设 2026/10/19 5:50:07

基于BP神经网络与MATLAB的交通标志识别与分类实现详解

简介:这是一份基于BP人工神经网络的交通标志识别与分类MATLAB项目源码,内置图形用户界面,面向计算机相关专业学生及课程设计、毕业设计场景,也可作为深度学习入门参考。压缩包内含31个文件,主要包括预训练模型&#xf…

作者头像 李华
网站建设 2026/10/19 5:49:56

MySQL触发器实战:从订单审计到性能陷阱的完整指南

这两天有个做订单系统的同学跑过来跟我抱怨:业务方总说订单状态变了,却说不清是谁改的。代码翻了一遍发现,更新状态的入口少说七八个,有的改了字段忘了写日志,有的记了日志忘了写操作人。这类问题我在项目里见过不止一…

作者头像 李华