Docker从入门到实战:镜像、容器、Compose与数据卷全解析
“在我电脑上是好的”——只要做过开发或运维大概率听过或者说过这句话。项目本地跑得飞起部署到服务器就各种崩新同事入职第一天光配Java、Node、MySQL、Redis的环境就耗掉大半天线上和测试环境版本稍微差一点行为就完全不同。这些痛苦的根源说白了就是五个字环境不一致。Docker 就是冲着这个问题来的。它把应用本身和它依赖的运行环境打包成一个标准单元让你在笔记本、测试机、云服务器上用同一套方式跑起来。这篇文章面向刚接触 Docker、被“镜像、容器、仓库、数据卷”这些概念绕晕的同学从最基础的概念讲起一路走到安装部署、常用命令、真实项目落地和报错排查不走弯路直接给能抄的作业。1. 先搞清楚Docker解决了什么问题从“环境地狱”到集装箱思维1.1 传统部署为什么这么痛在没有容器的时候部署一个应用要面对的问题有很多。第一是环境差异。你在本地装的是 MySQL 8.0服务器上是 5.7某个 SQL 语法在本地正常在服务器上直接报错。你说“我本地能跑啊”但服务器不认产品也不认。第二是依赖冲突。同一台服务器上跑着 A 项目和 B 项目A 需要 Python 3.8B 需要 Python 3.11又跑着老项目的 php5.6 和新项目的 php8.2时间一长服务器环境就是一团乱麻没人敢动。第三是部署成本高。新同事入职要按文档一步步装 JDK、Maven、Redis、Nginx每一步都可能踩坑运气好半天搞定运气不好一整天就过去了。这些问题不是靠“更详细的部署文档”能解决的。哪怕文档写得再细不同机器、不同系统版本、不同已装软件总会让你遇到文档里没写到的情况。Docker 的解法是釜底抽薪把应用和它需要的系统库、运行时、配置全部打成包整个包在任何机器上行为一致。你的本地跑的是这个包服务器上跑的也是这个包那环境差异问题就不存在了。1.2 容器和虚拟机不是一回事有人可能会说这不就是虚拟机吗不是。虚拟机是硬件级虚拟化每台虚拟机都要装一个完整的操作系统占磁盘好几个 GB启动按分钟算资源开销非常大。容器完全不一样。容器共享宿主机的操作系统内核只隔离进程、文件系统和网络空间。打个比方虚拟机是给你单独租一套房子里面水电煤家具全都重新配一套容器是给你一个标准集装箱箱子里打包好应用和它需要的东西但船、发动机这些基础设施是大家共用的。下面这张表是两者的核心差异维度虚拟机容器启动速度分钟级秒级资源占用GB级别独占系统MB级别共享内核隔离级别硬件级完全隔离进程级隔离镜像大小通常几个GB通常几十到几百MB一台机器能跑的数量个位数到十几台几十上百个所以容器不是贬低虚拟机的替代品它是更轻量的选择。大多数应用场景下容器完全够用还便宜。1.3 镜像、容器、仓库三个最容易搞混的概念把这三个概念理解清楚Docker 就懂了一半。镜像是一个只读的、打包好的模板里面包含了运行一个应用所需要的全部文件比如代码、运行时、系统库、配置文件。它就像一个集装箱的设计图纸或者一个“安装包”。容器是镜像运行起来之后产生的实例。同一个镜像可以启动多个容器它们互相隔离。容器是可变的你在容器里改了文件、装了软件这些修改只影响这个容器。仓库是存放镜像的地方。最常见的是 Docker Hub 官方仓库相当于应用商店。你从仓库拉到镜像再用镜像启动容器。用一个不太严谨但很好懂的类比镜像是一张光盘里的内容容器是你把光盘放进光驱后正在运行的程序仓库是卖光盘的商店。你从商店买光盘docker pull然后放进光驱运行docker run。再补一个后期会很有用的概念镜像分层。一个镜像不是一整块死数据而是由很多只读层叠起来的。你基于某个镜像构建新镜像时只新增修改的层不用把整个系统从头再来一遍。这就是为什么 Docker 打包快、传输省流量的核心原因。2. 环境准备Windows、Ubuntu、CentOS 安装要点与启动失败排查2.1 WindowsDocker Desktop WSL2 方案Windows 上现在主流的装法是 Docker Desktop 加 WSL2 后端。安装之前先检查两件事。一是虚拟化开关。打开任务管理器切到“性能”标签右下角会显示“虚拟化已启用”还是“已禁用”。如果是禁用状态Docker Desktop 启动的时候大概率会报virtualization support not detected。这时候必须进 BIOS/UEFI找到 Intel VT-xIntel 平台或 AMD-VAMD 平台的选项把它设为 Enabled。不同品牌主板的 BIOS 界面差异很大但关键词搜“品牌型号 Enable Virtualization”基本都能找到。改完保存退出重新开机再检查一次。二是 WSL 功能。以管理员身份打开 PowerShell运行wsl --install这条命令会启用“适用于 Linux 的 Windows 子系统”和“虚拟机平台”两个功能并安装默认的 Ubuntu 发行版。装完之后重启然后运行wsl --version确认 WSL 版本是 2。如果输出提示 WSL 1就需要手动设置默认版本wsl --set-default-version 2然后下载安装 Docker Desktop Installer安装时勾选“Use WSL 2 instead of Hyper-V”。安装完启动Docker Desktop 会自己把环境配好。右下角鲸鱼图标变绿打开终端运行docker version能看到 Client 和 Server 两端信息就说明引擎正常。2.2 配置了 WSL2 但启动还是失败Docker Desktop 启动失败报错里除了virtualization support not detected另一个高频的是failed to connect to the docker api at npipe:////./pipe/dockerDesktopLinuxEngine这个报错的意思很直白客户端找不到 Docker 引擎。常见原因有三个。第一Docker Desktop 启动了但引擎还在启动中WSL2 环境冷启动本来就慢等十几秒再试一次。第二旧版 WSL 内核和 Docker Desktop 不兼容。去微软官网搜索下载“WSL2 Linux 内核更新包”安装后重启。第三LxssManager服务卡住了。以管理员身份打开 PowerShell运行wsl --shutdown net stop LxssManager net start LxssManager再启动 Docker Desktop 试试。这一步能解决很多重装都无法解决的启动问题值得记下来。2.3 Ubuntu一条命令能装但“权限坑”必须绕开Ubuntu 上最简单的装法是直接用系统源里的包sudo apt update sudo apt install -y docker.io sudo systemctl enable --now docker装完先别急着用这里有个几乎每个人都会踩的坑直接运行docker ps会报 permission denied。原因是 Docker 引擎使用的是/var/run/docker.sock这个 socket 文件而它默认属于 root 组。解决方案是把当前用户加进 docker 组sudo usermod -aG docker $USER关键点执行完这条命令后必须注销重新登录或者重新开启终端组权限才会生效。很多人以为不用重启然后跑docker ps还是报错就开始怀疑人生。有些生产环境对版本要求高需要用 Docker 官方源安装 docker-cesudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod ar /etc/apt/keyrings/docker.gpg然后把仓库写入 apt 源再sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin。系统自带的 docker.io 和官方 docker-ce 功能上有差异官方版更新更及时生产环境建议用 docker-ce。2.4 CentOS旧版本升级的注意事项CentOS 上如果服务器里已经有旧版本 Docker不要直接覆盖安装先卸载干净sudo yum remove -y docker docker-client docker-common docker-engine老的包名和文件残留会影响新版本启动。清理之后装 docker-ce 的步骤是sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin sudo systemctl enable --now dockerCentOS 7 是老环境内核版本偏低部分场景下容器网络功能会受限。如果遇到容器内 DNS 解析有问题或者网络不通先检查宿主机内核版本是否低于 3.10过低就建议先升级内核或者换新系统不要硬刚。3. 镜像与容器的第一课常用命令与生命周期管理3.1 镜像操作pull、images、rmi、tag镜像相关的命令数量不多但每个都频繁使用。拉取镜像用docker pull。默认从 Docker Hub 拉取可以指定版本标签docker pull nginx:latest docker pull mysql:8.0 docker pull redis:7.0不写 tag 默认是 latest。建议生产环境尽量写明版本号因为 latest 会变今天拉的和三个月后拉的可能不是同一个镜像这是环境不一致的隐患之一。查看本地镜像docker images输出里 IMAGE ID 是镜像的唯一标识这串 ID 是镜像内容的哈希同一个镜像在不同机器上算出的 ID 完全相同这也是 Docker 能保证环境一致的根本原因。删除镜像docker rmi 镜像ID或名字如果镜像正被某个容器使用需要先删容器再删镜像或者加-f强制删除但我不太建议用-f容易误删还在用的东西。给镜像打标签docker tag nginx:latest myregistry.example.com/nginx:v1tag命令不会复制镜像只是给同一个镜像加了别名常用于推送私有仓库前打上仓库地址前缀。docker search也可以用来在命令行直接搜索 Docker Hub 上的镜像比如docker search mysql不过实际使用中我基本在网站上直接搜命令行的内容展示比较有限这里只做了解。3.2 容器操作run、ps、exec、logs、cp、rm容器是真正干活的单元。用 nginx 走一遍全流程最快建立手感。启动一个容器docker run -d --name web1 -p 8080:80 nginx拆解一下这个命令-d后台运行不加的话终端会卡在前台打印日志。--name web1给容器起个名字后续操作都用这个名字比记容器 ID 方便。-p 8080:80把宿主机的 8080 端口映射到容器内的 80 端口。容器是独立网络空间外部访问不到它内部的 80必须有这一层映射。nginx使用的镜像名。运行之后浏览器访问http://localhost:8080能看到 nginx 的欢迎页。查看容器列表docker ps docker ps -adocker ps只看运行中的容器docker ps -a会连已经退出的容器一起列出来。排错时务必用-a因为故障容器往往是退出状态的。进入容器内部查看docker exec -it web1 bash-it是交互终端的意思进入后你会发现自己在一个精简版 Linux 里。容器内不一定有 vim、curl 这些命令所以别依赖容器内调试工具常用做法是直接在宿主机上测试映射端口。查看日志docker logs web1 docker logs -f web1-f是持续跟随输出相当于tail -f。排错第一步永远是看日志这个习惯要养起来。从容器复制文件出来docker cp web1:/usr/share/nginx/html/index.html ./index.html这个命令在容器异常、需要抢救文件时非常好用。停止、启动、重启、删除docker stop web1 docker start web1 docker restart web1 docker rm -f web1容器删除后容器内做的修改会全部消失这也是下一章开始要讲数据卷的原因。3.3 为什么我 docker run ubuntu 什么反应都没有这是新手必踩的坑。执行docker run -d ubuntu用docker ps -a一看容器状态是 Exited。原因很简单容器不是虚拟机它不会自己“待机”容器里必须有一个前台进程在跑这个容器才会保持 running 状态。ubuntu 基础镜像默认没有常驻进程启动完就退出了所以容器也结束了。nginx 能一直跑是因为它启动后有个前台进程监听 80 端口。理解了这一点你就知道为什么很多镜像的 Dockerfile 最后总要写CMD [nginx, -g, daemon off;]这类命令——就是要保持前台状态。如果只是临时想用 ubuntu 容器执行几条命令可以这样docker run -it --rm ubuntu bash--rm表示容器退出后自动删除临时环境不残留垃圾。4. 拉镜像慢不是玄学镜像加速换源与自建私有仓库4.1 为什么 docker pull 总是慢到怀疑人生Docker Hub 的服务器在海外国内直连的话网络链路长、跨洲带宽小拉一个几百 MB 的镜像经常要几十分钟还动不动就超时报错报错信息里出现最多的就是EOF和timeout。解决办法不是去折腾网络而是配置镜像加速器。国内几家云厂商都做了 Docker Hub 的缓存节点你在配置里填上它们的地址拉取请求会走这些节点速度和稳定性都有明显提升。4.2 配置 registry-mirrors 的完整流程在 Linux 服务器上修改 Docker 的守护进程配置/etc/docker/daemon.jsonsudo mkdir -p /etc/docker sudo vim /etc/docker/daemon.json内容如下{ registry-mirrors: [ https://docker.m.daocloud.io, https://docker.mirrors.ustc.edu.cn, https://mirror.ccs.tencentyun.com ] }保存后重载配置sudo systemctl daemon-reload sudo systemctl restart docker验证是否生效docker info | grep -A 4 Registry Mirrors能看到刚填的地址列表就说明生效了。这里有一个很多人会忽略的点daemon.json 是 JSON 文件任何一个地方多一个逗号、少一个引号整个文件就会解析失败Docker 服务可能起不来。改完配置一定先跑docker info看一下如果引擎报错回到 json 格式检查。建议先用python3 -m json.tool /etc/docker/daemon.json校验格式再重启服务。如果你用的是阿里云还可以登录阿里云容器镜像服务控制台在“镜像加速器”页面拿到一个专属加速地址格式类似https://xxxx.mirror.aliyuncs.com。这个地址只对当前账号有效但速度非常稳。4.3 配置提速之后还是慢的几个应急手段第一设置镜像源不等于所有镜像都加速。对于一些冷门镜像缓存节点里没有还是得回源拉该慢还是慢。这种场景只能等。第二明确 tag 再拉。一个镜像如果默认拉 latest可能包含所有架构和平台的层体积巨大。比如docker pull mysql和docker pull mysql:8.0.33后者因为明确了版本拉的层会少一些。另外可以用--platform linux/amd64指定平台来减少多余层。第三多个源轮换。一个源访问量大了或者临时故障换另一个源很可能就恢复正常了。多填几个地址并不会降低速度Docker 会按顺序尝试。4.4 自建私有仓库registry 镜像的使用企业项目一般不会把镜像推到公共 Docker Hub通常自建私有仓库。用 Docker 官方 registry 镜像就能快速搭一个。docker run -d \ --name registry \ -p 5000:5000 \ -v /opt/registry:/var/lib/registry \ registry:2然后把本地镜像打上仓库地址前缀并推上去docker tag myapp:latest localhost:5000/myapp:v1 docker push localhost:5000/myapp:v1 docker pull localhost:5000/myapp:v1如果仓库跑在带域名的服务器上客户端需要配置 insecure-registries 才能通过 HTTP 直接拉取。daemon.json 里加上{ insecure-registries: [registry.example.com:5000] }这个配置会降低安全性生产环境建议用证书配置 HTTPS此处不再展开。总之私有仓库解决了团队内镜像分发问题和代码仓库的道理是一样的。5. 数据卷与端口映射让 MySQL、Redis 这类“有状态”服务跑起来5.1 为什么容器里的数据不能裸奔容器是可丢弃的。docker rm -f一执行容器就没了容器里写入的数据也跟着没了。这就像一个临时工位离职的时候东西全清空。数据库这种必须持久化的东西如果直接把数据写在容器里一次误删就是事故。标准做法是数据卷。数据卷把宿主机的一个目录挂载到容器内目录容器内写数据实际落在宿主机上容器删了数据还在换个容器重新挂载同一个目录数据就回来了。数据卷有三种方式对比如下方式写法使用场景bind mount-v /宿主机绝对路径:/容器路径挂载配置文件和宿主机数据目录named volume-v 卷名:/容器路径由 Docker 管理数据目录适合长期存储tmpfs--tmpfs /容器路径临时数据重启丢失不落盘我最常用的是 bind mount因为它直观宿主机上的路径看得见、摸得着备份、迁移都很方便。5.2 MySQL 8.0 从零部署数据卷、配置、密码一个都不能少创建一个目录结构然后启动容器mkdir -p /opt/mysql8/data /opt/mysql8/conf docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD你的密码 \ -v /opt/mysql8/data:/var/lib/mysql \ -v /opt/mysql8/conf/my.cnf:/etc/mysql/conf.d/my.cnf \ --restartalways \ mysql:8.0解释几个关键点-e MYSQL_ROOT_PASSWORD你的密码首次初始化时设置 root 密码。MySQL 镜像在第一次启动时会读取这个环境变量初始化完再改就晚了。如果漏了就要清掉数据卷重新初始化。-v /opt/mysql8/data:/var/lib/mysql把 MySQL 的数据目录挂到宿主机。这一步保证容器删除重建后数据还在。-v /opt/mysql8/conf/my.cnf:/etc/mysql/conf.d/my.cnf以单个文件方式挂载自定义配置。注意挂载单个配置文件时必须保证宿主机上的文件已经存在否则 Docker 会创建一个目录导致容器内挂载点变成目录而报错。--restartalways宿主机重启后容器自动启动。生产环境必加不然机器一重启你的数据库就失联了。my.cnf 里我一般至少会放两张配置[mysqld] character-set-serverutf8mb4 default-time-zone08:00 [client] default-character-setutf8mb4容器启动后用客户端连接验证docker exec -it mysql8 mysql -uroot -p这里有个经典坑MySQL 8.0 默认认证插件是caching_sha2_password如果你用的是比较老的 MySQL 客户端5.x 甚至更早或者古早版本的图形化工具会报认证失败。解决办法是升级客户端或者给专门的应用用户指定mysql_native_passwordCREATE USER app% IDENTIFIED WITH mysql_native_password BY 密码; GRANT ALL PRIVILEGES ON *.* TO app%; FLUSH PRIVILEGES;注意 root 默认只允许 localhost 登录如果应用需要远程连数据库应该创建专用账号并授权而不是直接去改 root 的 host 属性这是数据库安全的基本原则。5.3 Redis 部署单节点和主从配置先跑一个最简单的单节点mkdir -p /opt/redis/data docker run -d \ --name redis \ -p 6379:6379 \ -v /opt/redis/data:/data \ --restartalways \ redis:7.0 \ redis-server --appendonly yes--appendonly yes是开启 AOF 持久化数据会定期写到/data目录配合数据卷持久化才可靠。如果只想在内存里做缓存、丢数据也无所谓持久化可以不开。真正要在生产环境部署一般会用主从架构。手动用 docker run 配置主从需要先准备两个配置文件。master 的 redis.confbind 0.0.0.0 port 6379 requirepass yourpassword appendonly yes dir /dataslave 的 redis.confbind 0.0.0.0 port 6379 replicaof redis-master 6379 masterauth yourpassword appendonly yes dir /data然后分别启动两个容器。你会发现手动启动时replicaof后面跟的主机地址不好写用容器 IP 又不稳定。这个痛点非常适合用下一章的 docker compose 来解决。所以 Redis 主从的具体启动命令我放到 Compose 那章里完整演示。6. Compose 编排与微服务落地从单条命令到项目级部署6.1 为什么需要 Compose多个容器的“指挥中心”一个稍微像样点的项目通常不止一个容器。后端一个、数据库一个、Redis 一个、Nginx 一个如果再加上消息队列、定时任务五六个容器是常事。每个都用docker run手动启动意味着要记住五六条长命令还要保证启动顺序正确网络互通。时间久了没人知道这些容器当初是怎么跑起来的。Docker Compose 就是把一组容器的配置写进一个 YAML 文件用一条命令全部启动。好处很明显配置是代码可以进 Git、可以 review、可以在新机器上一条命令复现环境。这才是 Docker 环境一致性的最终形态不只是应用打包一致连部署编排也一致。现代 Docker 安装方式自带 compose 插件直接用docker compose命令即可不需要单独安装docker-compose。6.2 redis 生产环境部署的 compose 文件逐行拆解直接给一份我常用的 Redis 主从部署配置version: 3.8 services: redis-master: image: redis:7.0 container_name: redis-master restart: always ports: - 6379:6379 volumes: - ./redis-master/data:/data - ./redis-master/redis.conf:/usr/local/etc/redis/redis.conf command: [redis-server, /usr/local/etc/redis/redis.conf] networks: - redis-net redis-replica: image: redis:7.0 container_name: redis-replica restart: always ports: - 6380:6379 volumes: - ./redis-replica/data:/data - ./redis-replica/redis.conf:/usr/local/etc/redis/redis.conf command: [redis-server, /usr/local/etc/redis/redis.conf] depends_on: - redis-master networks: - redis-net networks: redis-net: driver: bridge逐个字段解释versioncompose 文件格式版本。新版 Docker 已经支持省略这个字段我习惯保留兼容性更好。services定义要启动的服务列表每个服务对应一个或多个容器。image使用的镜像。如果本地没有compose 会先拉取。container_name自定义容器名。不写的话 compose 会生成一个“项目名服务名序号”的容器名。restart: always容器退出或宿主机重启后自动拉起。生产必配。ports宿主机端口和容器端口映射格式是宿主机:容器。注意这里用的是字符串格式不要写成裸数字否则 YAML 解析可能有歧义。volumes数据卷挂载和docker run -v用法一致。command覆盖镜像默认启动命令。Redis 镜像默认不加参数启动是无密码、无持久化这里显式指定配置文件路径。depends_on声明服务启动依赖关系。从节点会等主节点先启动但这个字段只控制启动顺序不保证主节点完全可用。如果应用启动后立刻连接依赖服务仍然可能需要重试机制。networks把服务加入自定义网络。compose 会自动创建这个网络服务之间可以用服务名直接互通。比如从节点配置文件里的replicaof redis-master 6379这个redis-master就是服务名Docker 内置 DNS 会把它解析成主节点的容器 IP。启动整个环境docker compose up -d查看状态和日志docker compose ps docker compose logs -f redis-replica停止并清理docker compose downdown默认会保留数据卷数据不会丢这个设计非常合理。6.3 微服务项目的 compose 思路依赖关系与网络隔离微服务项目部署到 Docker 里的套路基本固定。每个服务对应一个镜像数据库、缓存、消息队列这些基础设施也各自是一个服务通过 compose 统一编排。核心工作有两个一是把服务间调用关系理清二是把配置抽到 environment 里。一个典型后端项目的 compose 片段services: mysql: image: mysql:8.0 container_name: order-mysql environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: order volumes: - ./mysql/data:/var/lib/mysql networks: - backend redis: image: redis:7.0 container_name: order-redis command: [redis-server, --requirepass, ${REDIS_PASSWORD}] networks: - backend app: build: . container_name: order-app ports: - 8080:8080 environment: DB_HOST: mysql DB_PORT: 3306 REDIS_HOST: redis REDIS_PORT: 6379 depends_on: - mysql - redis networks: - backend networks: backend: driver: bridge注意 environment 里用了${MYSQL_ROOT_PASSWORD}这种变量引用值从同目录下的.env文件读取。这样密码和配置不用硬编码进 compose 文件进 Git 的时候也更安全。应用配置里的数据库地址直接写服务名mysql、redis而不是 IP。因为 compose 网络内的 DNS 会自动解析服务名到对应容器 IP而且 IP 可能随容器重建发生变化但服务名不变。如果应用里还在用 IP 连接数据库容器一重建就断连这是很多人排错很久才发现的坑。6.4 Dockerfile 与镜像打包从部署者走向构建者搞明白 compose 之后你自然会有需求自己的应用怎么做成镜像答案是 Dockerfile。Dockerfile 就是一份构建镜像的配方。以一个 Node.js 应用为例FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install --registryhttps://registry.npmmirror.com COPY . . EXPOSE 3000 CMD [npm, start]几个核心指令FROM基础镜像。基于什么镜像构建尽量选官方镜像和 alpine 精简版体积小。WORKDIR设置容器内工作目录后续命令都会在这个目录下执行。COPY把宿主机文件拷进镜像。RUN构建镜像时执行的命令一般是安装依赖、编译等。EXPOSE声明容器要监听的端口只是声明实际映射还要靠-p或 compose。CMD容器启动时执行的命令只能有一个。然后执行构建docker build -t order-app:v1 .构建完就可以docker run或者写进 compose 里image: order-app:v1。顺便说一句热搜词里那个“IDEA 打包 Docker 镜像”本质也还是 Dockerfile 构建。IDEA 的 Docker 插件只是做了可视化操作把docker build的过程包装了一下。你理解了 Dockerfile用插件也好、命令行也好思路都是一样的。PHP、Java 项目的 Dockerfile 核心思路也一样无非是基础镜像换成 php:8.2-fpm-alpine、eclipse-temurin:17-jre再调整一下安装依赖和启动命令。7. 高频报错排查手册从现象到根因的思路链7.1 先看一张高频报错速查表报错/现象根因处理方向Virtualization support not detected虚拟化未开启或 VBS 冲突BIOS 开启 VT-x/AMD-V关闭内核隔离Failed to connect to the docker api at npipeDocker 引擎未启动或 WSL2 损坏重启 Docker Desktop重装 WSL 内核permission denied while trying to connect to the Docker daemon socket用户不在 docker 组sudo usermod -aG docker $USER后重新登录Error response from daemon: driver failed programming external connectivity端口被占用换端口或杀掉占用进程pull access denied / repository does not exist镜像名打错了或私有仓库未登录检查镜像名docker login后重试容器启动后立刻 Exited容器没有前台进程或启动命令报错docker logs 容器名看日志改启动命令镜像拉取一直 EOF 或 timeout网络到 Docker Hub 链路不稳配置镜像加速多源轮换7.2 排查报错的核心思路先看引擎、再看容器、最后看代码面对任何一个 Docker 报错不要急着百度按三层递进排查能解决 90% 的问题。第一层引擎是否正常。运行docker version如果客户端能连上引擎 Server 端输出正常说明引擎没问题。如果 Server 端报错优先重启服务sudo systemctl restart dockerWindows 上就重启 Docker Desktop。很多诡异的报错重启一次就消失了这不是玄学是 Docker 服务和 WSL2 之间偶尔会状态不一致。第二层容器是否正常。运行docker ps -a看目标容器状态。Exited 就一条命令看原因docker logs 容器名 --tail 100日志里一般会直接告诉你启动失败的原因。比如 MySQL 启动失败十有八九是配置写错或数据目录权限不对日志里会写[ERROR]开头的明确信息。第三层应用代码或网络是否正常。容器内测试连通性docker exec -it 容器名 bash然后在容器内尝试连接依赖服务。比如应用连不上数据库就在应用容器里先测curl 数据库服务名:端口或者用 ping 验证网络用 mysql 客户端验证认证信息。这一步能很快定位到底是代码问题、网络问题还是认证问题。7.3 磁盘空间和日志清理早晚会遇到的事Docker 跑久了镜像、容器、数据卷、构建缓存会占大量磁盘空间。docker system df可以查看空间占用分布docker system df清理悬空资源不再被容器使用的镜像层、构建缓存等docker system prune注意这个命令只会清理没有容器引用的资源不会删除正在运行的容器和数据卷相对安全。如果连未被使用的数据卷也要清docker system prune -a --volumes这条命令非常危险会把所有不被容器引用的镜像和数据卷全删了。如果数据卷是手动备份存的删了就真没了。用之前一定确认清楚。在容器日志不断增长导致磁盘满的场景下建议在 daemon.json 里加一句日志限制{ log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 } }这表示每个容器日志文件最大 10 MB最多保留 3 个自动轮转。不加这个配置一个不打印日志但运行异常的服务很可能一天就打满磁盘。7.4 一个典型的排查链路从报错到定位拿 MySQL 容器启动后立即退出这个场景演示完整排查链路。第一步看状态docker ps -a发现mysql8容器状态是 Exited。第二步看日志docker logs mysql8 --tail 50日志显示[ERROR] [MY-010457] [Server] --initialize specified but the data directory has files in it。这说明数据目录不是空的。原因通常是我之前强调过的那个坑挂载配置时宿主机目标路径缺失Docker 自动创建了目录而 MySQL 的初始化脚本认为目录非法。第三步定位查看宿主机挂载目录内容发现存在一个my.cnf目录而不是文件。清理干净改成正确挂载重新启动rm -rf /opt/mysql8/conf/my.cnf docker rm -f mysql8 # 先补配置文件再重新跑 docker run容器启动成功。整个过程没有玄学核心就是“状态 - 日志 - 配置”这条链路。养成这个习惯之后大部分问题都能自己解决不用动不动就删库重装。我用了这几年 Docker最大的感受是它的入门门槛不高但坑也不少而且大部分坑都是类似的。先理解镜像、容器、仓库、数据卷这四个基础概念再把命令用熟最后学会看日志和 compose 编排基本就能覆盖日常开发和部署的绝大多数场景。不要一上来就背命令先把一条完整链路走通比如从这个周末开始把 MySQL 和 Redis 用容器方式部署一遍这台机器的环境以后想坏都难。