你见过那只背着集装箱的鲸鱼吗?从本地开发、CI测试到生产部署,它几乎是环境问题的最优解。Docker,这个让人又爱又恨的容器引擎,新朋友的第一反应往往是“我为什么要用它”,老朋友则会问“为什么又连不上网了”。我一直觉得Docker很像清代考生写八股:固定的格式、固定的套路,但只要你学会那套起承转合,后面的路会异常顺。所以这期“范进说八股”,就专门把Docker的“八股”讲透,从安装、拉镜像、装MySQL8、搭Redis主从到网络排查,一条条说清楚。适合刚入门容器的新手,也适合已经在用、但偶尔被疑难杂症卡住的中间用户。这篇文章里没有玄学,只有我实际踩过的坑和验证过的方案。
1. Docker到底是什么,为什么值得信任
1.1 从“环境地狱”说起:镜像、容器、仓库
Docker火起来是因为一段经典对话:开发说“在我机器上是好的”,测试说“我这边起不来”,运维说“我部署上去就崩”。问题多半出在环境差异上——操作系统版本不同、依赖库版本不同、配置文件漏改。Docker的思路,是把“应用+运行环境”整个打包成一个镜像。镜像是静态文件集合,跑起来之后变成容器,相当于一个标准规格的隔离环境。你本地怎么跑,服务器上就怎么跑,测试环境和开发环境看到的是同一个“系统”。
仓库(Registry)这个概念也不难理解。它就像代码仓库,只是存放的是镜像。公共仓库里有大量官方镜像和社区镜像,本地没有的镜像拉取下来就行,你只需要docker pull mysql:8.0,甚至直接docker run mysql:8.0,Docker就会自动下载。这条“仓库—镜像—容器”链路,是整个Docker世界的骨架,也是八股里最核心的三股。很多人一开始觉得抽象,其实换个角度想,它就是一套标准打包规范,凡是把运行环境打包进去的东西,换个地方都能原样还原。
1.2 为什么Docker比虚拟机轻:共享内核、分层文件系统
有人会问,那Docker和虚拟机有什么区别?虚拟机要模拟完整硬件,安装Guest OS,启动慢、资源占用高,就像搬家时把整栋楼的装修都搬过去。Docker不一样,它不虚拟化硬件,直接复用宿主机内核,通过Linux内核的命名空间做进程、网络、文件系统隔离,用cgroups做资源限额。换句话说,虚拟机是搬来一个标准房间,Docker是自带标准行李箱住酒店,你只带需要的东西,酒店的基础设施直接复用。
所以容器启动通常在秒级,一台机器同时跑几十个容器都很轻松,这和跑几十个虚拟机是两个量级。Docker镜像还有一个特点:分层文件系统。多个容器可以共享同一份基础镜像层,只维护自己那份可写层。这带来一个很重要的习惯:不要在运行中的容器里改文件、装软件,格式化操作应该通过重新构建镜像完成。理解了这一层,后面看到“重启容器后临时文件就丢了”就不会太惊讶,毕竟容器设计初衷就是无状态、可销毁、可重建。
1.3 哪些场景值得“永远相信”这只鲸鱼
需要澄清一下,Docker不是银弹。我这些年最常用的场景有几类:本地开发环境,比如一台机器同时装MySQL和Redis,不会污染主机;微服务架构,几十个服务用docker compose或更上层的编排工具统一拉起;CI/CD流水线,在构建环节统一跑测试和打包;以及临时工具,比如需要一个特定版本的Nginx、MongoDB,用完就扔。
但Docker也有边界。对实时性要求极高的计算、强依赖宿主机内核模块的软件、需要极致磁盘性能的场景,容器未必是最优解。GUI应用、特殊硬件直通,往往也需要额外配置。写这一小节,是想让大家把预期摆正:Docker是环境管理工具,不是性能优化工具,更不是架构银弹。搞清楚了它能做什么、不能做什么,后面踩坑的概率才低。
2. 工具选型解析:Docker Desktop还是纯命令
2.1 各平台安装前的准备
安装Docker这件事,没有想象的难,但平台差异很明显。在Linux上,Docker是原生应用,通过包管理器安装,服务由systemd接管,命令行的体验最完整;在macOS和Windows上,最省事的是Docker Desktop。Docker Desktop底层其实还是一个Linux虚拟机,Windows上通过WSL2或Hyper-V运行,macOS上通过Apple虚拟化运行,所以虚拟化能力必须可用。
Windows用户安装前先确认三件事:BIOS里虚拟化确实开启;Windows 10/11的Hyper-V和WSL2组件状态正常;电脑里有没有其他虚拟化软件同时占用硬件虚拟化资源。Apple Silicon用户要选arm64版本,Intel用户选x86_64版本,选错很容易出现启动异常。Linux用户其实不需要Docker Desktop,直接装Docker Engine就好。如果你习惯Windows下的WSL2,在WSL2里装Docker Engine体验也不错,不过想要图形界面和更顺滑的配置体验,Desktop仍然是很多人的首选。
2.2 “Virtualization support not detected”到底怎么救
这个报错在Windows上高频出现,完整信息是:Virtualization support not detected. Docker Desktop failed to start because virtualization support is disabled or not present. 第一次遇到时,很多人会以为安装包坏了,其实核心就是“虚拟化没有启用”,Docker Desktop没有地方可跑Linux虚拟机。
排查顺序很重要。第一步,重启进入BIOS,把VT-x或AMD-V、SVM这类虚拟化选项打开。第二步,以管理员身份运行PowerShell,执行Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All,再执行bcdedit /set hypervisorlaunchtype auto,重启。第三步,检查WSL2是否正常,执行wsl --status,如果提示内核缺失,就下载安装WSL2内核更新包。第四步,暂时关闭或卸载VMware、VirtualBox、当前未使用的安卓模拟器,避免抢占虚拟化资源。为了方便照做,我整理成一张速查表:
| 步骤 | 操作 | 验证方式 |
|---|---|---|
| 1 | 重启进BIOS开启VT-x/AMD-V | 任务管理器性能栏显示“虚拟化:已启用” |
| 2 | 启用Windows可选功能Hyper-V | 可选功能列表勾选Hyper-V后重启 |
| 3 | 检查WSL2状态 | 执行wsl --status看Default Version是否为2 |
| 4 | 关闭其他虚拟化软件 | 退出或卸载VMware/VirtualBox后重试 |
还有一个容易被忽略的原因:Windows安全中心“内核隔离”里的内存完整性,可能和Docker Desktop的虚拟化兼容性冲突。可以在“设备安全性—内核隔离”中暂时关闭它再试。这些步骤做完,绝大多数“启动失败”都能解决。如果还不行,就去事件查看器里看Docker Engine日志,或者干脆卸载干净重装一遍,但多数情况不用走到那一步。
2.3 安装后必须做的三件事
验证安装不是打开图标看一眼,建议做三件事。第一,终端执行docker --version和docker compose version,确认客户端和服务端都在。第二,执行docker run hello-world,这个极小的镜像会打印一段成功提示。这一条命令同时验证了守护进程、网络拉取、容器运行三个环节。第三,配置镜像加速。国内直接拉公共仓库镜像偶尔会慢到怀疑人生,Docker Desktop的设置里可以增加registry-mirrors配置,保存后重启Docker。有两点提醒:所有公共加速服务都有可能失效或限速,不必过度依赖;生产环境建议自建私有仓库,把构建流程的关键依赖固化下来,而不是把命运全押在公共网络上。
3. 核心细节解析与实操要点:部署MySQL8并用起来
3.1 一条命令跑起MySQL8
MySQL绝对是Docker系列里必考的题目。官方镜像完整名是mysql:8.0,直接用它。最简单的单机命令如下:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=YourStrongPass@2024 \ -e TZ=Asia/Shanghai \ -v mysql_data:/var/lib/mysql \ -v /etc/localtime:/etc/localtime:ro \ mysql:8.0逐项拆开讲。--name mysql8是容器名,后面docker logs和docker exec都用得上;-p 3306:3306把宿主机3306端口映射到容器内部的3306端口,外部要访问MySQL就靠它;MYSQL_ROOT_PASSWORD是官方镜像要求的环境变量,第一次启动时用来初始化root用户密码;TZ=Asia/Shanghai解决了容器默认UTC的时间差,否则数据库里记录的时间和本地时间会差8个小时。两个-v是重点:mysql_data:/var/lib/mysql把数据存到Docker具名数据卷里,容器删了数据还在;/etc/localtime挂载成只读,保证日志时区和宿主机一致。这种docker run方式适合快速测试,但如果是项目级部署,我更推荐Compose。
有人会问,为什么不用宿主机直接装MySQL,非得套个容器?因为容器能把这套依赖全部隔离起来,换机器、换环境都保持一致,数据卷又解决了持久化。如果本机3306已经被占用,把端口映射改成-p 3307:3306就行,容器内部依旧监听3306,外部通过3307访问。端口映射不是越多越好,只在确有需要时暴露。
3.2 为什么我更推荐docker compose管理MySQL
docker run适合一次性启动,但真实项目里,数据库参数往往需要团队共用、代码审查、版本回溯。docker compose可以把一堆参数写成文件,别人克隆项目后执行docker compose up -d就能得到一样的环境。比如我给MySQL常用的Compose配置长这样:
services: mysql8: image: mysql:8.0 container_name: mysql8 restart: always environment: MYSQL_ROOT_PASSWORD: YourStrongPass@2024 TZ: Asia/Shanghai ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql - /etc/localtime:/etc/localtime:ro command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci networks: - app_net volumes: mysql_data: networks: app_net: driver: bridge这段配置比docker run多出的东西,核心是声明式管理。restart: always让Docker在守护进程重启或容器异常退出时自动拉起MySQL;command项可以给MySQL追加启动参数,这里把字符集固定成utf8mb4,从根上避免中文乱码和emoji写入失败;networks定义了自定义桥接网络,后面如果还有Redis等服务加入同一网络,就能直接用服务名访问,不用写死IP。mysql_data没有宿主路径,说明这是具名数据卷,由Docker托管。好处是干净、跨平台,坏处是文件位置相对隐蔽,需要备份时可以docker volume inspect看它到底落在宿主机哪个目录。
3.3 客户端连不上MySQL的排查口诀
这是Docker使用中最常见、也最容易让人烦躁的环节。工具装了、容器起了,Navicat或DataGrip却报Access denied或者Can't connect。我总结成一套口诀:一查状态,二看日志,三试密码,四验端口,五清防火墙。
容器状态:docker ps -a,如果容器是Exited状态,看docker logs mysql8,密码或目录权限问题通常会在日志里露出马脚。日志里有“ERROR 1045”大概率是密码错误,可以先docker exec -it mysql8 bash进入容器,用mysql -uroot -p试一遍环境变量里的密码。容器内能登录、外部工具不行,检查docker port mysql8看端口映射是否生效;宿主机本机连接还需要确认MySQL外部访问权限,默认root用户通常只允许从localhost连,外部连接被拒不是Docker的问题,是MySQL用户授权表的问题。
常见做法是进入容器执行SQL,给root或新建用户设置允许任意主机访问,并指定认证插件:
CREATE USER 'app'@'%' IDENTIFIED WITH mysql_native_password BY 'AppPass@2024'; GRANT ALL PRIVILEGES ON *.* TO 'app'@'%'; FLUSH PRIVILEGES;注意MySQL8默认的caching_sha2_password对很多老版本驱动和可视化工具有兼容性问题,mysql_native_password是临时兼容的常见选择,但安全要求高的项目建议升级驱动,不要长期依赖旧插件。最后别忘两件事:宿主机防火墙放行3306,云服务器安全组放行3306。很多人本地全通,一换云服务器就连不上,大概率是因为安全组规则没改。
4. 实操过程与核心环节实现:Redis主从部署
4.1 Redis容器化部署的主从需求
单机Redis再稳,也怕机器挂掉。主从复制是Redis高可用的最低配:一台主节点写,一台或多台从节点同步,主节点出问题时至少能切到从节点、留个后手。用Docker搭主从,最大的好处是环境一致,不用在服务器上手动编译、配置、启动两个进程,而且Docker容器之间天然可以通过网络互相访问,非常适合拆成多个实例。
需要注意Redis官方镜像的时区、配置文件目录,以及主从复制的开启方式。最简单的方法是在从节点容器启动时加--replicaof参数,指定主节点的IP和端口。不过在Compose里,容器IP是动态分配的,不应该写死,而是用服务名。比如我们把两个服务分别叫redis-master和redis-slave,从节点配置里写:
command: redis-server --replicaof redis-master 6379这样Docker内置DNS会直接把redis-master解析到主容器IP,比写死IP稳定得多。如果主节点设置了密码,从节点同步时还得加上--masterauth,否则复制会卡住,日志会报NOAUTH。
4.2 先手动跑两个Redis容器,理解底层过程
在引入Compose之前,建议手动操作一次,这样遇到问题才不慌。先启动主节点:
docker run -d --name redis-master \ -p 6379:6379 \ redis:7 redis-server --requirepass masterpass再启动从节点:
docker run -d --name redis-slave \ -p 6380:6379 \ --link redis-master \ redis:7 redis-server \ --requirepass masterpass \ --replicaof redis-master 6379这里用了--link,这是老版本容器互通方式,生产环境不推荐,自定义网络才是正道,这里只是为了先看通链路。然后去从节点验证:
docker exec -it redis-slave redis-cli -a masterpass info replication只要看到role:slave、master_link_status:up,复制链路就通了。你还可以在主节点set一个key,再去从节点get,能看到同步完成。这个验证思路,比任何解释都直观。主从不是主备,主节点挂掉后从节点不会自动接管,那是哨兵或集群该干的事,但复制链路通了,后续再往高可用扩展就顺理成章。
4.3 用docker compose写一份Redis主从
手动跑清楚后,Compose就顺理成章了。下面的配置我实际部署过,足以作为起点:
services: redis-master: image: redis:7 container_name: redis-master restart: always command: redis-server --requirepass masterpass --appendonly yes ports: - "6379:6379" volumes: - redis_master_data:/data networks: - redis_net redis-slave: image: redis:7 container_name: redis-slave restart: always depends_on: - redis-master command: redis-server --requirepass masterpass --replicaof redis-master 6379 --masterauth masterpass --appendonly yes ports: - "6380:6379" volumes: - redis_slave_data:/data networks: - redis_net volumes: redis_master_data: redis_slave_data: networks: redis_net: driver: bridge这里把AOF持久化打开了,容器删除后数据卷里还有数据,Redis能恢复。执行docker compose up -d后,可以这样验证:
docker compose ps docker exec -it redis-slave redis-cli -a masterpass info replication一个主从架构几分钟就能跑起来。很多人会在这时候想加哨兵,但哨兵至少要3个实例独立部署,做故障自动切换,可以留到后续单独聊。先把主从这条链路理清楚,它是一切高可用的地基。
5. 常见问题与排查技巧实录
5.1 Docker网络不通的几种典型情况
网络不通是所有Docker使用者的必经之路。常见的表现有三种:宿主机访问不到容器、容器访问不到宿主机、容器之间访问不到。
宿主机访问不到容器,先检查端口映射。容器默认使用桥接网络,宿主机要通过-p暴露的端口访问,而不是直接访问容器IP。容器访问不到宿主机,最常见的原因是容器里的localhost是容器自己,不是宿主机。要在容器里连接宿主机上的数据库或服务,需要区分平台:Linux上可以尽量用网关地址,Windows和macOS上可以直接用宿主机IP。同时还要确认宿主机服务绑定的地址,不能是127.0.0.1,否则容器自然访问不到。
容器之间访问不到,最常见原因是它们不在同一个自定义网络。docker run启动的容器默认进默认桥接网络,一旦用了自定义网络,就得保持同一网络或用服务名互通。这也是为什么我一直强调用Compose:同一个Compose文件里的服务默认在同一网络,直接通过服务名互相通信,省去配IP的麻烦。
5.2 容器启动失败/退出状态码怎么看
容器一遍遍重启或者一闪就没,别直接删了重建,先看状态码和日志。docker ps -a能看到Exited (状态码)。状态码1通常指应用启动命令失败,比如MySQL初始化脚本出错;137说明进程被kill,多半是内存超限或者手动docker kill;139很像段错误。看到状态码后,执行docker logs 容器名,看尾部日志,大多数线索都在里面。
还可以用docker inspect 容器名查看RestartCount。如果restart策略是always,容器失败后会不停重启,很容易被“运行中”的假象骗到。这种情况建议临时把restart策略关掉,先把日志调出来看。我遇到过一种很诡异的情况:容器启动不到一秒就退出,日志为空,最后发现是镜像启动脚本依赖了宿主机的特殊环境变量。这种问题只能缩小范围,用docker run -it 镜像名 /bin/bash手动进容器,逐个排查,没有比这更快的捷径。
5.3 数据卷挂载的坑
把容器环境和数据分离,是Docker的重要思想,但挂载卷的坑也不少。最坑的是宿主目录权限。如果挂载-v /my/own/datadir:/var/lib/mysql,宿主机目录必须提前存在或为空,MySQL初始化时如果目录权限不对,会报chown: changing ownership of '/var/lib/mysql': Operation not permitted,然后启动失败。解决方法是给宿主机目录设置正确的用户ID,或者直接用具名数据卷,让Docker管理目录权限。
另一个坑是挂载单个文件时宿主机文件不存在。比如-v /opt/redis.conf:/etc/redis/redis.conf,如果宿主机/opt/redis.conf不存在,Docker会自己创建一个目录,Redis启动时直接报错。解决办法是先touch创建文件,再挂载。还有一个容易忽略的点:MySQL数据卷已经有数据后,你再改MYSQL_ROOT_PASSWORD环境变量,密码不会生效,因为初始化脚本只在数据卷首次为空时执行。这也是很多人“明明设置了密码,怎么总是连接失败”的原因。
5.4 磁盘占满与清理三板斧
跑一段时间Docker后,几个G的空间莫名其妙消失。先执行docker system df,看镜像、容器、数据卷、构建缓存分别占了多少。清理命令我一般按三个级别执行:
docker image prune:清理悬空镜像,也就是那些不再被任何容器引用的镜像。docker container prune:删除所有已停止的容器,谨慎起见也可以先docker rm挨个删。docker system prune -a -f:清理所有未被使用的镜像、容器、网络和构建缓存,这是大扫除。注意它会删除所有没有在用的镜像,重新拉一次可能比较费时间。
数据卷不要轻易用docker volume prune,它会删除所有未被容器使用的卷,数据可能找不回来。清理之前,先把数据备份好。另外镜像分层会缓存,每次构建时复用layer,如果想减小镜像体积,检查一下是否把日志、临时文件之类的大文件打进了构建上下文,用.dockerignore提前过滤。
最后再分享一个小技巧。很多朋友以为Docker是个黑盒,其实它有一套很朴素的“八股”:不理解时就docker inspect把容器配置拉出来看,遇到问题时docker logs永远是最可靠的证人。我自己折腾Docker这几年的最大体会,不是它解决了多少环境问题,而是它逼着你把依赖、端口、数据卷、网络这些本该交代清楚的事情,全部明确写下来。正因为有了这套结构化的“八股”,那只鲸鱼才值得被信任。下次再看到Docker报错,别慌,先按格式拆解,拆着拆着你会发现,自己已经开始懂它了。