从“在我电脑上是好的”这句名言说起吧。干开发这些年,我见过太多应用部署时炸得一塌糊涂的场景:代码在开发环境跑得好好的,到了测试环境就开始闹脾气,到了生产环境直接罢工。排查到最后,通常就是那么一句——环境不一样。版本差一点、依赖缺一个、系统库版本对不上,问题就冒出来了。Docker就是来解决这件事的,它是目前把“应用+运行环境”打包交付最成熟的方案,没有之一。
这篇内容对应的是Docker学习的第一课时,适合刚接触容器、在虚拟机和管理服务器之间反复折腾、被环境问题搞到头大的开发者。不需要你有任何容器基础,只要能在命令行里敲几条命令就行。我会尽量把原理讲明白,把每一步的命令解释清楚,也把我自己早期踩过的坑一并交代了。读完你应该能独立完成Docker的安装、镜像拉取、容器启停、端口映射和数据持久化,并且对镜像、容器、仓库这三个核心概念建立起正确的心智模型。
1. Docker到底解决什么问题:从“环境地狱”到标准化交付
1.1 环境不一致的经典场景
先还原一个几乎人人都遇到过的场景。你写了一个Python服务,本地Python 3.11,依赖全装好,跑起来一切正常。交付给同事的时候,他机器上是Python 3.7,某个第三方库的新语法在老版本上直接报错;或者他的系统自带OpenSSL版本偏老,加密相关的接口全部抛异常。这种时候你会怎么做?折腾半天装依赖、升级系统库,最后源代码没改一行,时间全耗在环境上面。
往大了说,从开发到测试再到生产,每一层的操作系统、运行时、依赖库、配置文件都存在差异。传统部署方式里,环境一致性是靠运维人员手工保证的,是人就会出错,一出错就是事故。Docker的思路从根本上改变了这件事:把应用和它依赖的完整运行环境一起打包成一个镜像,这个镜像在哪儿跑,行为都是一样的。
1.2 Docker是怎么解决环境问题的
Docker利用Linux内核的命名空间和控制组特性,实现了进程级别的隔离。命名空间让容器里的进程拥有自己独立的视图,像是独立的文件系统、独立的网络栈、独立的进程树;控制组则负责限制容器能使用的CPU、内存等资源。你可以把容器想象成一台台迷你电脑,它们共享宿主机的操作系统内核,但彼此看不见对方内部的东西。
换个更通俗的说法,虚拟机是把一整个操作系统完整地虚拟出来,所以体积大、启动慢,通常要按分钟算;容器则只是把宿主机的内核当作底座,在上面叠加一层精简的用户态运行环境,所以镜像体积小、启动快,往往秒级完成。这也是为什么现在很少有人在生产环境为了跑一个应用专门开一台虚拟机了——资源浪费太严重。
1.3 学Docker之前先建立的两个认知
第一个认知是镜像一旦构建,内容就不再变化。你可以把镜像理解成一张光盘,内容压制好了就是只读的,不能直接在原盘上改。想改怎么办?基于它创建容器去里面改,改完再提交成新镜像。这保证了同一个镜像跑出来的容器,初始状态绝对一致。
第二个认知是容器本身是临时的。容器在运行过程中产生的所有文件变更,默认都保存在容器那层可写层里。一旦容器被删除,这些变更全部消失。这不一定是缺陷,反而是一种特性:你随时可以从镜像重新起一个全新的容器,恢复初始状态,这给自动化运维带来了极大的便利。但这也意味着,你真正需要保留的数据必须通过挂载卷的方式放到宿主机上,这个后面会细说。
2. 镜像、容器、仓库:课时1必须搞清的三个核心概念
2.1 镜像(Image):只读模板
镜像是一个只读的、分层结构的文件系统快照,里面包含了运行应用所需的全部内容:基础操作系统层、代码、运行时、依赖库、环境变量和默认命令。这里最关键的概念是分层存储。
Docker镜像由多层组成,每一层都是前一层之上的一个增量修改。比如你基于Ubuntu镜像装了一个Python运行时,这就新增了一层;接着往里拷代码,又是新的一层。拉取镜像时Docker会识别本地已有的层,只下载缺少的层,这就是为什么你只下载过一次基础镜像、后面基于它构建新镜像时会很快的原因。
这种分层设计不只是省流量,更重要的是层可以复用。十个不同的应用都在同一个基础镜像上构建,宿主机只需要保存一份基础镜像的物理数据。Docker通过写时复制策略将只读层共享给所有容器使用,极大节省了磁盘空间。
2.2 容器(Container):镜像的运行时实例
容器是镜像运行起来之后的实例。镜像本身是静态的磁盘文件,容器则是一个正在运行、拥有自己文件系统和进程空间的动态实体。运行容器的时候,Docker在镜像的只读层之上追加了一个可写层,应用的所有写入操作都在这个可写层进行。
把镜像和容器的关系类比成程序和进程就很贴切:程序是保存在磁盘上的静态文件,进程是程序加载到内存后运行起来的实例;镜像就是这个静态的“程序安装包”,容器就是“正在运行的程序实例”。一个镜像可以同时启动多个容器实例,彼此互不干扰,各跑各的。你也可以随时删除某个容器再重新创建,只要镜像还在。
2.3 仓库(Registry):镜像的分布渠道
仓库专门用来存储和分发镜像。最常用的是Docker官方的Docker Hub,此外还有各大云厂商提供的加速器或私有仓库。镜像的完整名称通常由仓库地址、镜像名和标签三部分组成:比如nginx:1.25,nginx是镜像名,1.25是标签,省略地址时默认是从Docker Hub拉取。
这里有一个容易混淆的地方,仓库(Registry)和仓库内的镜像(Repository)在英文里是同一个词根,但在具体操作中,主要关注两条命令就够了:push把本地镜像推到远端仓库,pull把远端镜像拉到本地。对于初学者,完全可以先把仓库理解成一个镜像的“网盘”,先用好pull,等自己会构建镜像了再研究push。
3. 环境准备与安装:在Windows、macOS和Linux上跑起第一个容器
3.1 Windows与macOS安装要点
Windows平台要求系统必须支持并开启硬件虚拟化,Win 10 64位家庭版及以上版本基本都能满足。安装流程其实被官方大大简化了:下载Docker Desktop安装包,双击安装,全程默认配置即可。安装完成后首次启动,Docker Desktop会提示你启用WSL 2后端,建议选“是”。
WSL 2是Docker在Windows上运行时强烈推荐的引擎,它的内存占用和启动速度都要优于传统虚拟化方案。早期版的Docker Desktop直接跑Hyper-V虚拟机,那个内存占用简直是灾难,现在默认的WSL 2方案收敛了很多。安装WSL 2需要两步:先在PowerShell管理员模式下执行wsl --install,重启后再在Docker Desktop里开启集成选项。macOS的流程几乎一样,下载Docker Desktop的dmg文件安装即可,Apple Silicon芯片的机器记得下载对应芯片架构的安装包。
3.2 Linux安装与加速配置
Linux上没有桌面版,直接在命令行用包管理器安装。以Ubuntu系为例,官方推荐的流程是添加Docker官方软件源,然后安装docker-ce、docker-ce-cli和containerd.io这三个包。安装完成后,把当前用户加入docker组:
sudo usermod -aG docker $USER然后退出当前终端重新登录,之后运行docker命令就不需要加sudo了。这一步经常被新手跳过,结果每次执行docker命令都要加sudo,不但麻烦,还可能因为权限问题导致一些连接docker引擎的操作失败。
Linux下更关键的一步是配置镜像加速器。国内直连Docker官方仓库经常面临拉取超时或者速度极慢的问题,我最早用Docker的时候,拉一个Ubuntu镜像能卡十几分钟,最后直接超时。解决方案是编辑/etc/docker/daemon.json,填入你自己申请到的加速地址:
{ "registry-mirrors": ["https://你的加速器地址"] }配置完成后重启Docker服务:
sudo systemctl daemon-reload sudo systemctl restart docker3.3 验证安装:hello-world背后发生了什么
安装完成后运行一条最经典的命令来验证:
docker run hello-world这条命令会让Docker在本机镜像缓存里找hello-world,找不到就去Docker Hub拉取,拉取成功后创建容器并运行。容器启动后输出一段欢迎信息就退出了。如果这段欢迎信息正常打印出来,说明你的安装是通的。
很多人第一次跑这条命令只记住了“安装成功”,却没意识到Docker完成了一整套“拉镜像-建容器-跑命令-退出”的完整链路。你可以把这套机制想象成:你去餐厅点了一份套餐,后厨从柜子里拿出标准化的预制菜加热,端上桌,吃完走人,桌上恢复原样。这份预制菜就是镜像,加热上桌就是你平时启动容器做的事情,吃完走人则是容器停止。这个基本流程,就是之后所有Docker操作的地基。
4. 核心生命周期操作:跑、停、查、删的完整闭环
4.1 拉取镜像
先建立镜像缓存池,再从池子里取镜像运行。拉取镜像是日常最高频的操作之一:
docker pull nginx:1.25 docker pull python:3.11-slim docker pull ubuntu:22.04关于tag选择,我诚恳地建议:生产环境必须写具体版本号,绝不可以用latest,否则某一天基础镜像上游更新了,你的应用会因为环境变化出现莫名其妙的问题。我自己的一个早期线上事故就是latest标签导致的:上游镜像更新了一个小版本,某个底层库的行为发生了变化,服务第二天开始随机报错,排查了整整一下午。
4.2 创建并启动容器
最基本的运行命令:
docker run -d --name my-nginx -p 8080:80 nginx:1.25逐段拆解一下:
-d:后台运行容器,终端不会被占住--name my-nginx:给容器命名,后续管理时不需要记住容器ID-p 8080:80:端口映射。宿主机8080端口收到的流量转发到容器的80端口,80是nginx在容器内默认监听的端口nginx:1.25:镜像名和标签
运行后用docker ps查看,看到my-nginx的状态是Up,说明容器正在运行。浏览器访问http://localhost:8080,就能看到nginx的欢迎页面。
这里有个非常实用的知识点:docker run是“创建并启动”的合并操作。如果容器已经存在,就不能再用run,而应该用docker start。run、start、restart这三个命令对应着“新建并运行”“启动已存在的停止容器”“重启”,它们的行为不一样,一开始很容易混。
4.3 查看、停止、删除
日常巡检和执行操作的高频命令:
docker ps # 只显示正在运行的容器 docker ps -a # 显示所有容器,包括已停止的 docker stop my-nginx # 停止容器,相当于给进程发停止信号 docker start my-nginx # 启动已停止的容器 docker restart my-nginx # 重启容器 docker rm my-nginx # 删除容器删除镜像用的是另一个命令:
docker rmi nginx:1.25这里有一个初学者经常困惑的问题:docker rm删的是容器,docker rmi删的是镜像。两个命令差一个字母,作用完全不同。删镜像前要保证没有正在运行的容器还在使用它,否则会报错。还有一个更彻底的大扫除命令:
docker system prune它会清理掉所有已停止的容器、未被使用的网络和悬空镜像。加了-a参数还会把没有被任何容器引用的镜像一并清掉,磁盘空间瞬间释放不少。不过这条命令要谨慎使用,它会确认删除一堆东西,在共享机器上别随便执行。
4.4 exec进入容器:第一次在容器里执行命令
容器虽然是隔离的,但可以通过exec进入其中执行命令,这是调试和实际操作的必备手段:
docker exec -it my-nginx bash执行后你就进入了容器的交互式Shell。在容器里随便执行ls、cat /etc/nginx/nginx.conf看看内容。想退出就执行exit。-it这两个参数是有讲究的:-i保持标准输入打开,让命令可以接收你的键盘输入;-t分配一个伪终端,让输出正常显示。两者组合在一起,才能获得一个可以正常交互的终端体验。
不交互地执行单条命令也行:
docker exec my-nginx nginx -v这条命令把nginx -v直接传进容器执行,输出nginx版本信息后就结束。这种一次性命令在自动化脚本里非常有用。
5. 端口映射与数据持久化:让容器真正“能用”起来
5.1 端口映射:-p参数的完整理解
默认情况下容器有自己独立的网络,宿主机访问不到容器里的服务。要让外部能访问到容器内进程,必须做端口映射。-p 8080:80的本质,是宿主机上的一个小程序监听8080端口收到请求后,转发给容器内的80端口。
这种转发带来的一个直接好处是可以在同一台机器上跑多个同一镜像的容器实例,端口错开就行了:
docker run -d --name web-1 -p 8081:80 nginx:1.25 docker run -d --name web-2 -p 8082:80 nginx:1.25修改宿主机端口时要注意别和其他服务冲突,建议执行netstat -tlnp先查一下端口占用情况。另外,-p支持多个,像-p 8080:80 -p 8443:443这种写法,同时映射HTTP和HTTPS端口。
5.2 数据卷:-v参数与容器数据的持久化
前面说过,容器默认是临时的,删了就什么都没了。解决这个问题的方法是使用数据卷(Volume),把宿主机上指定的目录挂载到容器内部指定目录,这样容器内的写入操作会直接落在宿主机上。举个例子:
docker run -d --name my-nginx -p 8080:80 \ -v /home/me/nginx-html:/usr/share/nginx/html \ nginx:1.25宿主机上/home/me/nginx-html目录里的文件,就是nginx容器/usr/share/nginx/html目录下能看到的文件。直接编辑宿主机里的文件,刷新浏览器就能看到效果,再也不用进容器里改文件了。
这个机制理解起来可以类比成:容器是一台临时工位的电脑,数据卷则是那个跟工位无关的外接移动硬盘。不管电脑换几台,硬盘里的数据一直在。
5.3 环境变量:-e参数的最简用法
很多镜像在启动时会读取环境变量来决定运行行为。以运行一个最简单的MySQL为例:
docker run -d --name my-mysql \ -e MYSQL_ROOT_PASSWORD=my-secret-pw \ -p 3306:3306 \ mysql:8.0启动MySQL镜像时你必须在环境变量里指定root密码,否则容器会创建失败。-e参数就是把环境变量传入容器内部,容器内进程就能读取到。环境变量提供了一种很方便的配置方式,避免反复修改配置文件。实际项目中还有个常用技巧:把环境变量写入文件,用--env-file参数整体加载,方便管理多个配置项。
6. 课时1最容易踩的四个坑与自检清单
6.1 坑一:Windows下Docker Desktop启动失败
排查思路是先看系统是否开启了虚拟化功能。打开任务管理器,性能标签页里能看到“虚拟化”一项是否显示“已启用”。如果没启用,需要进BIOS打开Intel VT-x或AMD-V。另一个常见原因是WSL 2没有正确安装,可以在PowerShell里执行wsl --status查看状态。这类问题最常见的根源就是系统组件缺失,而不是Docker本身坏了。
6.2 坑二:docker pull超时或速度极慢
这个在国内环境几乎必现。解决办法就是前面说的配置镜像加速器。注意daemon.json改完之后要重启docker服务才会生效。如果配置了加速还是慢,换一个地址试试。大型镜像也可以在深夜网络空闲时段拉取,实测下来的确快不少。
6.3 坑三:容器退出后数据没了
经常有人问我:“我跑了个Redis,存了测试数据,容器删了重新创建后数据全没了,Redis是不是不支持持久化?”问题不在Redis,而是默认容器就是个临时环境。解决方法是启动时挂载数据卷,把数据目录映射到宿主机上。Redis这类应用要在容器启动前就规划好数据卷,等数据丢了再想起来就晚了。
6.4 坑四:镜像体积越积越大
镜像、容器、构建缓存会一点一点蚕食磁盘空间。有个简单办法,定时清理不用的资源:
docker system df这条命令能直观看到镜像、容器、数据卷分别占了多少磁盘。占用异常时用docker system prune -a大扫除一把。删除单个容器时用docker rm -f可以强制删除正在运行的容器,省去先stop再rm的步骤,但别养成习惯,生产环境还是要按流程来。
6.5 课时1自检清单
你可以用这份清单测试一下这课时有没有真正吃透:
- 能否解释镜像和容器的关系,以及为什么同样的镜像可以启动多个不同状态的服务
- 能否解释
docker run -d -p 8080:80 --name web nginx这条命令里每个参数的含义 - 容器删除后,数据去哪里了?用什么手段可以让数据保留在宿主机上
docker rm和docker rmi各自的作用是什么- 容器启动后如果报错了,用什么命令查看日志和排查问题
排查问题这一步很重要,具体命令是docker logs my-nginx,它会输出容器内部主进程的日志。大多数容器把自己运行时的状态都写在标准输出里,看日志是定位问题的第一选择。
课程内容到这里基本结束了。结合我自己的学习路线给一条建议:不要急着深挖网络模式、Docker Compose和镜像构建,先把docker run用熟,把镜像、容器、卷这三大模块的概念彻底搞透。这些是容器世界的地基,地基稳了,后面学Compose编排、K8s集群时你会觉得一切都是自然而然的。
我自己当时花了大概一周时间反复练习这些基础命令,每天把同一个服务部署三五个变体,在不同端口上跑、挂载不同的目录、注入不同的环境变量,练完这些之后再看高级特性,理解速度明显加快。建议你也准备一台虚拟机或云服务器,动手练一遍,比看什么都管用。