装过Docker的人都知道,第一次把docker run hello-world跑起来,屏幕上打出一段"Hello from Docker!"的时候,心里那点成就感是真的。但等你回过神来,往往是一连串问号:镜像和容器到底啥关系?为什么我照着教程装MySQL,一会儿密码不对,一会儿数据一重启就没了?端口映射那串-p参数到底是怎么个映射法?
这篇文章就是冲着这些问号来的。我尽量用干过活的口吻,把Docker的底层逻辑、安装过程里那些容易卡住的细节、以及新手最容易踩的坑串起来讲一遍。不保证你看完能立刻成为容器专家,但至少装完环境、跑通第一个真实容器、遇到常见报错时不慌。内容主要面向刚接触容器化技术的开发者、运维新手,以及那些被"环境不一致"折磨到想换电脑的师兄师姐们。
1. 容器化这件事,到底在解决什么问题
1.1 从"在我电脑上是好的"说起
做开发的几乎都被这句话坑过:代码在自己机器上跑得好好的,一部署到测试环境、生产环境,行为就完全不一样。原因无非那么几个:操作系统版本不同、依赖库版本对不上、配置文件差异、甚至环境变量缺失。传统解决方式是写一堆部署文档,让运维照着一步步操作,然后被各种细枝末节折腾到崩溃。
虚拟化技术出现后,好歹能把整个环境封装成一个虚拟机镜像,但虚拟机太笨重了——它要装完整操作系统,占用好几个GB磁盘空间,启动要几十秒甚至几分钟。一台物理机跑三五个虚拟机就差不多了,资源开销极其浪费。
1.2 容器和虚拟机到底差在哪
容器和虚拟机最本质的区别在于:虚拟机虚拟的是硬件层,每个虚拟机都要运行一个完整的客户操作系统(Guest OS);而容器虚拟的是操作系统层,所有容器共享宿主机内核,只是把用户空间的文件、依赖、配置隔离打包。
可以这么理解:虚拟机是把整个"房间"独立出来,装修、家电、水管全给你整套,房间之间彻底隔离,但代价是每间房都要配一套完整的水电系统。容器更像是把"家具家电"打包成标准集装箱,直接放在同一栋楼里,大家共用同一套水电骨架,但家里怎么布置自己说了算。
这个区别带来三个直接优势:
- 镜像尺寸小:一个基础镜像可能就几十MB到几百MB,常用Docker镜像大多数都能在几分钟内拉取完成。
- 启动速度快:容器启动本质上是启动一个进程,秒级甚至毫秒级。
- 资源密度高:一台8核16G的机器,跑二三十个容器是很正常的,虚拟机早就卡死了。
当然,容器也有短板:因为共享内核,容器隔离性弱于虚拟机,不适合需要强隔离的敏感场景;Windows容器和Linux容器也不能混着跑(内核不同)。但在绝大多数应用打包、开发环境统一、微服务部署场景下,容器的优势是压倒性的。
1.3 Docker的三大核心概念:镜像、容器、仓库
这三个概念是Docker的地基,必须掰开揉碎讲清楚。
镜像(Image):一个只读的、分层存储的文件模板。镜像包含完整的应用代码、运行时、系统库、配置等一切运行所需的东西。镜像的分层结构很有讲究:每一层是只读的,层的叠加构成最终文件系统。比如mysql:8.0镜像,底层可能是debian基础层,上面叠加MySQL安装层、配置层、数据目录层。设计成层的最大好处是复用——多个镜像可以共享底层相同的层,节省磁盘空间和传输带宽。
容器(Container):镜像是模板,容器就是模板创造出来的运行实例。容器是镜像最上层加了一个可写层(Container Layer)。你在容器里写的文件、改的配置,都落在这一层。容器可以启动、停止、删除,也可以基于同一镜像启动多个容器。要注意,容器一旦被docker rm删除,可写层里的数据也随之消失——这就是后面要讲"数据卷"为什么重要的原因。
仓库(Registry):用来存放和分发镜像的地方,类似代码仓库与Git的关系。Docker Hub是最知名的公共镜像仓库,公司内部通常会用Harbor建私有仓库。拉取镜像用docker pull,推送镜像用docker push,这些操作都是和仓库在打交道。
一句话串起来:从仓库拉取镜像,docker run镜像变成运行中的容器,容器里的数据和状态要么用数据卷持久化,要么销毁时一起消失。
2. Docker环境准备:不同平台怎么装最省心
2.1 Windows上安装Docker Desktop的关键一步
Windows装Docker Desktop,最常见也最致命的坑就是WSL2。新版Docker Desktop默认依赖WSL2后端,如果没提前装好WSL2,安装完成后启动会直接报错,类似Docker Desktop failed to start because virtualization support wasn't detected。
我在Windows上的安装顺序建议是这样的:
- 先以管理员身份打开PowerShell,执行:
wsl --install这条命令会自动安装WSL、WSL2内核,并把默认版本设为2。如果电脑里已有旧版WSL,可以先执行wsl --update升级。
重启电脑,确认
wsl --status显示默认版本为2。安装Docker Desktop,一路下一步即可。安装程序一般会自动检测到WSL2后端。
启动Docker Desktop,等右下角鲸鱼图标稳定下来,Settings里Resources -> WSL Integration确保勾选了你要用的发行版。
为什么要死磕WSL2?因为Docker Desktop在Windows下有两种后端模式:Hyper-V和WSL2。WSL2模式下容器运行在轻量级虚拟机中,内存占用更小、启动更快,和Windows的文件互通也更方便。而且WSL2是微软自己的方案,兼容性和后续更新都有保证。
2.2 macOS和Linux的安装差异
macOS上安装Docker Desktop更简单,从官网下载.dmg拖进Applications就好。注意区分Intel芯片和Apple Silicon(M系列)两种安装包,别下错了。M系列芯片可以直接跑amd64架构的镜像,Docker Desktop内部有模拟层,但性能上还是优先选择arm64版镜像。
Linux下的安装大家用得比较多的是两条路线:一是直接用发行版包管理器装docker.io或者官方源里的docker-ce;二是用 官方安装脚本 (仅推荐用于开发环境)。生产环境建议配置官方apt源或yum源安装docker-ce,方便版本管理和后续升级。
装完后最好不要直接用root跑docker命令,Docker Desktop替代方案要么把当前用户加入docker组:
sudo usermod -aG docker $USER newgrp docker这样以后执行docker ps就不用加sudo了。还要记得设置Docker守护进程开机自启:
sudo systemctl enable docker sudo systemctl start docker2.3 装完怎么验证环境真的OK
敲docker version,能看到Client和Server两部分信息,说明服务端在跑。如果只有Client信息,Server连接不上,那就得看Docker服务有没有启动。
再敲docker info,里面会输出大量环境信息:容器数量、镜像数量、存储驱动、CPUs、内存等等。其中Storage Driver这行值得注意,Linux一般显示overlay2,这是目前最推荐的存储驱动。
Windows用户还要在Docker Desktop的Settings里留意Resources配置,默认内存是2GB左右,跑MySQL、Redis、Node几个容器后常常不够,可以调到4GB或更高。
提示:如果Windows上启动Docker Desktop一直失败,可以查看
C:\Users\你的用户名\AppData\Local\Docker\log\下的日志。最典型的原因就是虚拟化功能没开启(BIOS里需要打开VT-x或AMD-V),或者Hyper-V功能未启用。
3. 装完之后,先跑通这5个必学命令
3.1 hello-world只是验证,不是入门
很多教程让你运行docker run hello-world,看到输出就算入门了。但说实话,这个镜像太小、太简单,它只是验证Docker引擎能正常拉镜像、能创建容器、能执行程序。真正的手感还得靠常用命令积累。
我建议新手按下面这条路径练习:pull->images->run->ps->exec->logs->rm。整套流程走完,才算摸到Docker的门把手。
3.2 镜像拉取、查看、删除的完整回路
拉取镜像没什么花头,就是docker pull 镜像名。镜像名默认会加latest标签,比如执行docker pull nginx,实际拉取的是nginx:latest。生产环境中最好指定明确的版本标签,比如nginx:1.25,否则latest指向的版本一旦变化,环境就不可控了。
查看本地镜像:
docker images输出里包含仓库名、标签、镜像ID、创建时间、大小。镜像ID是一串十六进制字符串,很多操作可以用短ID(前面几位)替代完整ID。
删除镜像用docker rmi:
docker rmi nginx:latest如果删除时提示镜像被容器占用,说明有容器正在用它或处于停止状态,需要先删容器再删镜像:
docker rm <容器名或ID> docker rmi <镜像名或ID>认识--rm这个参数也很有价值:docker run --rm ubuntu echo hello,容器退出后自动清理容器本身,适合做一次性任务,避免垃圾容器堆积。
3.3 容器生命周期管理:run、exec、stop、rm怎么配合
启动容器的基础姿势:
docker run -d --name test-nginx -p 8080:80 nginx参数逐个拆开讲:
-d:后台运行容器,不会占据当前终端。--name test-nginx:给容器起个名字,后续操作都可以用这个名字代替容器ID。-p 8080:80:端口映射,宿主机8080端口收到的请求转发到容器的80端口。-it:交互模式,配合-i(保持STDIN)和-t(分配伪终端)使用,典型场景是进入容器执行命令,比如docker run -it ubuntu bash。
进入容器有两个高频率命令:
docker exec -it test-nginx bash docker attach test-nginx两者区别:exec是在容器中开启一个新进程,退出时不影响主进程运行;attach是连接到容器的主进程终端,缺点是容器主进程一停,连接就断。日常调试推荐exec。
查看容器状态:
docker ps docker ps -adocker ps只显示运行中的容器,加-a才能看到停止状态的容器。新手常犯的错误是容器明明启动了却用docker ps找不到,多半是因为容器运行几秒就退出了(比如hello-world执行完就退出),不用慌,docker ps -a能看到历史。
停止和删除:
docker stop <容器名> docker rm <容器名>stop是优雅停止(发送SIGTERM,等待超时后再SIGKILL),kill是强制终止(直接SIGKILL)。一般优先用stop,让容器里的进程有机会做清理工作。
整个生命周期串起来就是:docker run创建并启动一个新容器,docker stop停止它,docker start再次启动同一个容器(注意,是start不是run),docker rm删除容器。这里新手很容易混淆:docker run是"创建+启动",docker start只是启动一个已存在的、处于停止状态的容器。
4. 从命令到实战:用Nginx和MySQL把知识串起来
4.1 Nginx容器:端口映射与静态站点托管
选Nginx做第一个实战对象很合适,因为镜像小、依赖少、跑起来立竿见影。
先拉取并启动:
docker run -d --name web-server -p 8080:80 nginx:1.25启动后在浏览器访问http://localhost:8080,看到Nginx欢迎页,说明端口映射链路通了。这一步就验证了宿主机端口和容器端口的对应关系:请求打到宿主机的8080,Docker在内核层面转发到容器的80端口。
光看默认页不过瘾,可以用挂载的方式替换Nginx的默认网页目录:
docker run -d --name web-server -p 8080:80 \ -v /home/user/my-site:/usr/share/nginx/html:ro \ nginx:1.25-v /home/user/my-site:/usr/share/nginx/html:ro把宿主机的my-site目录挂载到容器内的Nginx网页目录,:ro表示只读挂载,防止容器内修改宿主机文件。这样一来,改宿主机里的HTML,容器立即可见,不需要重新构建镜像。
这里顺便理解Nginx的日志查看方式:
docker logs web-server docker logs -f web-server-f是跟随输出,类似tail -f。Nginx的访问日志和错误日志都打到标准输出,正好被Docker收集起来。
4.2 MySQL容器:密码、数据卷、字符集的组合拳
MySQL容器比Nginx复杂一个维度,因为它在数据持久化、密码初始化、初始化脚本执行方面都有自己的讲究。
一个推荐的启动命令:
docker run -d \ --name mysql-server \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=admin123 \ -e MYSQL_DATABASE=appdb \ -e TZ=Asia/Shanghai \ -v /opt/mysql-data:/var/lib/mysql \ mysql:8.0逐个解释关键点:
-e传环境变量。MYSQL_ROOT_PASSWORD设置root密码;MYSQL_DATABASE会在首次启动时自动创建名为appdb的数据库;TZ=Asia/Shanghai把容器时区设为东八区,否则MySQL默认时间戳会落后8小时,容易让人误判业务数据的时间。-v /opt/mysql-data:/var/lib/mysql,把MySQL的数据目录挂到宿主机。这是所有数据库容器第一条"保命规则":没有它,容器一删,数据库片甲不留。3306:3306,显式映射端口。如果宿主机3306已被占用,可以换成3307:3306,外部用3307端口访问。
第一次跑MySQL,重点理解"容器初始化"过程:数据目录为空时,MySQL容器会自动执行初始化(创建系统表、设置root密码、执行环境变量定义的初始化动作),所以首次启动会慢一些。之后数据目录非空,环境变量就不再生效了,密码以数据目录里的实际设置为准。这个规律适用于几乎所有带持久化数据的容器。
验证一下连接:
docker exec -it mysql-server mysql -uroot -p在容器内执行客户端连接,走的是容器内部的socket,不需要网络。如果从宿主机连接,则需要用mysql -h127.0.0.1 -P3306 -uroot -p。注意,从宿主机连接时-h建议写127.0.0.1而不是localhost,否则客户端可能优先使用Unix socket而不是TCP,端口映射就不起作用了。
MySQL8.0默认的认证插件是caching_sha2_password,老客户端(比如非常旧的Python驱动)可能连不上,报"Authentication plugin 'caching_sha2_password' cannot be loaded"。解决办法是启动时加参数或改用户认证方式,但更省事的方法是用5.7版本应对老系统兼容需求。
4.3 一个容器乱丢日志?先学会资源查看
运行几个容器后,资源占用怎么查?
docker stats类似Linux的top,实时显示所有容器的CPU、内存、网络I/O、磁盘I/O。docker stats --no-stream只输出一次快照,适合脚本调用。
容器内部的进程和文件情况:
docker top mysql-server docker exec mysql-server ls -l /var/lib/mysql这里要注意,docker top和docker exec看到的进程视角不同:top看到的是宿主机视角下容器内的进程(PID是宿主机全局的),exec看到的是容器内命名空间视角。初学者不用过于纠结,知道两者存在就行。
排查容器为何高占用时,常见流程是:先docker stats定位哪个容器异常,再docker logs查应用日志,必要时docker exec -it 容器名 sh进入容器用系统工具进一步排查。
5. 新手最常见的5个坑与排查思路
5.1 WSL2未开启导致Docker Desktop启动失败
这个问题排名第一,因为我见过太多人栽在这里。报错信息很典型:Docker Desktop failed to start because virtualization support wasn't detected,以及virtualization support not detected。字面上说"没检测到虚拟化",实际原因往往是WSL2没装或者虚拟化功能在BIOS中被禁。
排查顺序:
- 打开PowerShell,执行
wsl --status,检查默认版本是不是2。 - 如果显示的是升级到版本1,执行
wsl --set-default-version 2。 - 按
Win+R输入optionalfeatures回车,勾选"适用于Linux的Windows子系统"和"虚拟机平台",重启电脑。 - 如果还是不行,进BIOS确认Intel VT-x或AMD-V虚拟化开启。笔记本电脑的虚拟化开关经常被出厂设置关掉。
5.2 端口冲突:明明映射了,怎么访问不到
场景很典型:docker run -p 8080:80 ...启动Nginx,浏览器访问localhost:8080却说连不上,或者页面上是另一个完全不同的应用。
第一反应是检查端口占用:
netstat -ano | findstr :8080 # Windows ss -lntp | grep 8080 # Linux如果确实被别的进程占用,干脆换映射端口重新跑容器,或先停掉占用端口的进程。另外注意Docker Desktop在WSL2模式下,端口映射是在虚拟网络层做的,偶发映射失效时,把Docker Desktop重启一般能解决。这个属于"经验性重启",但实测有效。
让我再提一个容易忽略的细节:docker run -p 8080:80之后,如果你已经有一个容器占用了8080,再启动新容器时会报Bind for 0.0.0.0:8080 failed: port is already allocated。这时不是简单停掉旧容器就行,而是要么换端口映射,要么确保旧容器完全删除。
5.3 镜像拉取超时或失败
Docker Hub在国内访问有时会出现"慢"或者Get https://registry-1.docker.io/v2/: net/http: TLS handshake timeout这类错误。解决思路有几种:
一是给Docker配置镜像加速器(registry mirror)。Docker Daemon配置文件通常是/etc/docker/daemon.json(Linux)或通过Docker Desktop的Settings界面配置(Windows/macOS)。
{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com" ] }配置后重启Docker,重新docker pull,速度和稳定性通常有肉眼可见的提升。生产环境对镜像源稳定性要求高的团队一般会用Harbor搭私有仓库,把基础镜像预置好,部署时直接内网拉取,彻底绕开公网波动。
二是拉取国外镜像时打了官方tag找不到,可以先去第三方镜像站搜索,但注意第三方镜像站的镜像完整性无法保证,不要用于对完整性要求极高的生产环境。
5.4 容器内无法访问外网
容器里ping不通外部域名,或者apt-get update失败,通常是网络模式或DNS问题。
先检查宿主机能不能正常联网。宿主机网络没问题的话,再排查容器DNS配置:
docker exec <容器> cat /etc/resolv.conf如果DNS配置异常,可以在docker run时显式指定DNS服务器:
docker run -d --dns 8.8.8.8 --dns 223.5.5.5 ...还有一种可能是容器使用host网络模式时端口映射方式不同(--network host下不需要也无效-p参数),DNS解析规则也会继承宿主机。遇到网络疑难杂症,先检查docker network ls查看网络列表,用docker network inspect bridge查看默认网络的配置,这是很有效的排障路径。
5.5 数据一重启就丢:数据卷没挂对
"我的MySQL重启后表没了""今天改的文件第二天就没有了"——这基本就是没用好数据卷。回顾第4节的MySQL例子,数据卷挂载之后,数据写的是宿主机上的/opt/mysql-data目录。容器删除重建,挂载目录不变,数据就还在。
正确的排查方法是docker inspect查看挂载情况:
docker inspect mysql-server | grep -A 5 Mounts输出里会列出Source和Destination的对应关系。Source是宿主机路径,Destination是容器路径。如果Source为空,说明用的是匿名卷或没有挂载数据卷。
对于非数据库类应用,通常建议用命名卷(named volume)管理持久化数据,因为不需要关心宿主机具体存储路径,Docker会自动维护:
docker volume create mydata docker run -d -v mydata:/app/data ...命名卷的优点在于对宿主机的路径不敏感,迁移和备份都通过docker volume操作即可。
6. docker compose:从单容器走向多容器编排
6.1 为什么需要compose
手敲docker run命令管理三五个容器勉强还能忍受。一旦容器数量到十几个,问题就来了:启动顺序怎么保证?依赖关系怎么处理?配置怎么统一管理?各家容器怎么互联?
docker compose(老版本叫docker-compose)就是来解决这个问题的。它用YAML文件描述一组容器的配置,一条命令拉起/停止整个应用栈。
很多"自研微服务"项目里,前端一个容器、后端两三个容器、MySQL一个、Redis一个,全部用compose编排,开发环境直接docker compose up -d就能复现整套环境。这种"基础设施即代码"的玩法,比手敲命令可维护性高得多。
6.2 一个compose文件编排Redis与Node应用
用一个实际案例说明:前端Node应用需要连Redis,我们通过compose把两者编排起来。
目录结构:
my-app/ ├── docker-compose.yml ├── node-app/ │ ├── Dockerfile │ └── app.js └── redis-data/docker-compose.yml内容:
version: "3.8" services: redis: image: redis:7.0-alpine container_name: my-redis ports: - "6379:6379" volumes: - ./redis-data:/data command: redis-server --appendonly yes node-app: build: ./node-app container_name: my-node-app ports: - "3000:3000" depends_on: - redis environment: - REDIS_HOST=redis - REDIS_PORT=6379里面有几个值得细品的点:
depends_on只是控制启动顺序,不等待服务就绪。如果Node应用启动时Redis还没完全初始化好,仍然可能连接失败。严格方案是用healthcheck探活,compose会在Redis健康后再启动依赖服务,但这属于进阶玩法。REDIS_HOST=redis直接用了服务名redis作为主机名。Compose会在项目自定义网络中为每个服务创建DNS记录,容器之间通过服务名互相访问,不需要暴露到宿主机的映射端口。这个内部网络是隔离的,只有声明了ports的端口才对宿主机开放。command: redis-server --appendonly yes会覆盖镜像默认的启动命令,这里开启AOF持久化,让Redis的数据也能落盘到./redis-data挂载目录。
启动方式:
docker compose up -d docker compose ps docker compose logs -f docker compose downdown会删除所有相关容器,但不会删除卷——数据卷默认保留。真要连卷一起清理,用docker compose down -v,慎用,这会清掉挂载数据。
这个编排方案的价值在于:团队新人拉下仓库,跑一遍docker compose up -d,整个环境和PR描述里的状态一模一样。不用再写"先装Redis,再配环境变量,再启动Node"这类文档了。
6.3 从compose出发,还可以往哪儿走
Compose适合单机多容器的编排,再往上就进入容器编排平台(如Kubernetes)的领域了。当容器数量跨过几十上百,需要分布式调度、弹性伸缩、服务发现、负载均衡时,Compose就力不从心了。但Kubernetes的学习曲线陡峭得多,我个人的建议是:先把Docker的基础命令和Compose玩透,理解镜像、容器、网络、数据卷这些核心概念,再逐步接触编排层的概念。直接从Kubernetes起步,如果没有Docker基础,很容易在镜像构建、Pod配置、存储挂载等环节一头雾水。
7. 实操时要留意的事
最后分享几条我在实际使用里积累的经验,不算教程本身,但能减少很多莫名其妙的时间消耗。
第一,容器命名别偷懒。Docker会自动生成一串随机的容器名,像keen_swanson这种,看着不直观,管理起来也费劲。每次创建容器都手动指定--name,一眼能认出是哪个服务,避免操作错误。
第二,镜像标签永远是构建时的关键依赖。在Dockerfile和compose文件里,镜像尽量固定版本号,不要用latest。我在生产环境吃过一次亏:latest标签被上游更新过,重新部署后行为变化,排查了很久才发现是基础镜像版本变了。锁定版本标签虽然保守,但可控性和回溯性都好得多。
第三,容器退出后记得清理。开发中频繁创建、删除容器很常见,偶尔会积累一堆停止状态的容器占用磁盘。一条命令清理不用的容器和镜像:
docker container prune -f docker image prune -f危险的版本是docker system prune -a,它会删除所有未被使用容器占用的镜像和构建缓存,磁盘空间被释放的同时,下次启动如果需要这些镜像还得重新拉取。执行之前确认一下输出内容再按回车。
第四,日志别积压。容器日志默认无限增长,长时间运行的容器可能吃掉大量磁盘。日志文件的默认位置在Linux是/var/lib/docker/containers/<容器ID>/下的*-json.log。建议在Docker Daemon配置里加上日志轮转限制:
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }配置后每个容器的单个日志文件最多10MB、保留3个文件,自动滚动,防止日志把磁盘撑爆。这类限制对运行超过一个月、日志量又大的服务特别重要。
第五,能用官方镜像就优先官方镜像。很多复杂中间件(Redis、MySQL、Nginx、Elasticsearch)都有官方镜像,配置参数、兼容性和文档都比第三方镜像可靠。官方镜像的入口脚本写得比较完整,比如MySQL镜像会在首次挂载空数据目录时自动初始化,这些都是第三方镜像不一定有的行为。等到需要对镜像做精简定制时,再基于官方镜像修改,风险会小很多。
写到这里,该打住的就打住了。Docker的入门路径其实不复杂:装好环境、跑通命令、用真实服务试一试、踩几个坑回头理解概念。等你哪天发现,身边同事还在为环境问题耗费几个小时,而你一条命令就拉起一个干净环境,就会明白这些折腾是值当的。